diff --git a/portal-ui/tests/permissions-7/deleteWithPrefixes.ts b/portal-ui/tests/permissions-7/deleteWithPrefixes.ts new file mode 100644 index 000000000..728b2fcdd --- /dev/null +++ b/portal-ui/tests/permissions-7/deleteWithPrefixes.ts @@ -0,0 +1,63 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2023 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import * as roles from "../utils/roles"; +import { Selector } from "testcafe"; +import * as functions from "../utils/functions"; +import { namedTestBucketBrowseButtonFor } from "../utils/functions"; + +fixture("Test resources policy").page("http://localhost:9090/"); + +const bucket1 = "abucket3"; +const test1BucketBrowseButton = namedTestBucketBrowseButtonFor(bucket1); +export const remainingFile = Selector( + ".ReactVirtualized__Table__rowColumn", +).withText("abcd"); + +test + .before(async (t) => { + await functions.setUpNamedBucket(t, bucket1); + await functions.setVersionedBucket(t, bucket1); + await functions.uploadNamedObjectToBucket( + t, + bucket1, + "abc", + "portal-ui/tests/uploads/noextension", + ); + await functions.uploadNamedObjectToBucket( + t, + bucket1, + "abcd", + "portal-ui/tests/uploads/noextension", + ); + })( + "Files with similar prefixes don't get deleted with all versions", + async (t) => { + await t + .useRole(roles.admin) + .navigateTo(`http://localhost:9090/browser`) + .click(test1BucketBrowseButton) + .click(Selector(".ReactVirtualized__Table__rowColumn").withText("abc")) + .click(Selector("#delete-element-click")) + .click(Selector("#delete-versions-switch")) + .click(Selector("#confirm-ok")) + .expect(remainingFile.exists) + .ok(); + }, + ) + .after(async (t) => { + await functions.cleanUpNamedBucketAndUploads(t, bucket1); + }); diff --git a/portal-ui/tests/uploads/noextension b/portal-ui/tests/uploads/noextension new file mode 100644 index 000000000..b24469c65 --- /dev/null +++ b/portal-ui/tests/uploads/noextension @@ -0,0 +1 @@ +a demo file diff --git a/restapi/user_objects.go b/restapi/user_objects.go index d127bf881..434f37403 100644 --- a/restapi/user_objects.go +++ b/restapi/user_objects.go @@ -854,17 +854,22 @@ func deleteObjects(ctx context.Context, client MCClient, bucket string, path str } if recursive || allVersions { - return deleteMultipleObjects(ctx, client, recursive, allVersions, bypass) + return deleteMultipleObjects(ctx, client, path, recursive, allVersions, bypass) } return deleteSingleObject(ctx, client, bucket, path, versionID, bypass) } +// Return standardized URL to be used to compare later. +func getStandardizedURL(targetURL string) string { + return filepath.FromSlash(targetURL) +} + // deleteMultipleObjects uses listing before removal, it can list recursively or not, // // Use cases: // * Remove objects recursively -func deleteMultipleObjects(ctx context.Context, client MCClient, recursive, allVersions, isBypass bool) error { +func deleteMultipleObjects(ctx context.Context, client MCClient, path string, recursive, allVersions, isBypass bool) error { // Constants defined to make this code more readable const ( isIncomplete = false @@ -892,6 +897,11 @@ func deleteMultipleObjects(ctx context.Context, client MCClient, recursive, allV if content.Err != nil { continue } + + if !strings.HasSuffix(getStandardizedURL(content.URL.Path), path) && !strings.HasSuffix(path, "/") { + continue + } + select { case contentCh <- content: case <-lctx.Done():