diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/DeleteMultipleObjects.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/DeleteMultipleObjects.tsx index 16724617b..8ef284084 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/DeleteMultipleObjects.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/DeleteMultipleObjects.tsx @@ -24,7 +24,10 @@ import { ConfirmDeleteIcon } from "../../../../../../icons"; import FormSwitchWrapper from "../../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper"; import { setErrorSnackMessage } from "../../../../../../systemSlice"; -import { useAppDispatch } from "../../../../../../store"; +import { AppState, useAppDispatch } from "../../../../../../store"; +import { hasPermission } from "../../../../../../common/SecureComponent"; +import { IAM_SCOPES } from "../../../../../../common/SecureComponent/permissions"; +import { useSelector } from "react-redux"; interface IDeleteObjectProps { closeDeleteModalAndRefresh: (refresh: boolean) => void; @@ -48,10 +51,22 @@ const DeleteObject = ({ const onDelError = (err: ErrorResponseHandler) => dispatch(setErrorSnackMessage(err)); const onClose = () => closeDeleteModalAndRefresh(false); - const [deleteVersions, setDeleteVersions] = useState(false); const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError); + const [deleteVersions, setDeleteVersions] = useState(false); + const [bypassGovernance, setBypassGovernance] = useState(false); + + const retentionConfig = useSelector( + (state: AppState) => state.objectBrowser.retentionConfig + ); + + const canBypass = + hasPermission( + [selectedBucket], + [IAM_SCOPES.S3_BYPASS_GOVERNANCE_RETENTION] + ) && retentionConfig?.mode === "governance"; + if (!selectedObjects) { return null; } @@ -76,7 +91,9 @@ const DeleteObject = ({ if (toSend) { invokeDeleteApi( "POST", - `/api/v1/buckets/${selectedBucket}/delete-objects?all_versions=${deleteVersions}`, + `/api/v1/buckets/${selectedBucket}/delete-objects?all_versions=${deleteVersions}${ + bypassGovernance ? "&bypass=true" : "" + }`, toSend ); } @@ -111,6 +128,28 @@ const DeleteObject = ({ }} description="" /> + {canBypass && deleteVersions && ( + +
+ { + setBypassGovernance(!bypassGovernance); + }} + description="" + /> +
+
+ )} {deleteVersions && (
. -import React, { useEffect, useState } from "react"; +import React, { Fragment, useEffect, useState } from "react"; import { DialogContentText } from "@mui/material"; import Grid from "@mui/material/Grid"; @@ -25,7 +25,11 @@ import ConfirmDialog from "../../../../Common/ModalWrapper/ConfirmDialog"; import api from "../../../../../../common/api"; import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; import { setErrorSnackMessage } from "../../../../../../systemSlice"; -import { useAppDispatch } from "../../../../../../store"; +import { AppState, useAppDispatch } from "../../../../../../store"; +import FormSwitchWrapper from "../../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper"; +import { hasPermission } from "../../../../../../common/SecureComponent"; +import { IAM_SCOPES } from "../../../../../../common/SecureComponent/permissions"; +import { useSelector } from "react-redux"; interface IDeleteNonCurrentProps { closeDeleteModalAndRefresh: (refresh: boolean) => void; @@ -43,13 +47,26 @@ const DeleteNonCurrentVersions = ({ const dispatch = useAppDispatch(); const [deleteLoading, setDeleteLoading] = useState(false); const [typeConfirm, setTypeConfirm] = useState(""); + const [bypassGovernance, setBypassGovernance] = useState(false); + + const retentionConfig = useSelector( + (state: AppState) => state.objectBrowser.retentionConfig + ); + + const canBypass = + hasPermission( + [selectedBucket], + [IAM_SCOPES.S3_BYPASS_GOVERNANCE_RETENTION] + ) && retentionConfig?.mode === "governance"; useEffect(() => { if (deleteLoading) { api .invoke( "DELETE", - `/api/v1/buckets/${selectedBucket}/objects?path=${selectedObject}&non_current_versions=true` + `/api/v1/buckets/${selectedBucket}/objects?path=${selectedObject}&non_current_versions=true${ + bypassGovernance ? "&bypass=true" : "" + }` ) .then(() => { closeDeleteModalAndRefresh(true); @@ -65,6 +82,7 @@ const DeleteNonCurrentVersions = ({ dispatch, selectedObject, selectedBucket, + bypassGovernance, ]); if (!selectedObject) { @@ -90,6 +108,28 @@ const DeleteNonCurrentVersions = ({ Are you sure you want to delete all the non-current versions for:{" "} {decodeURLString(selectedObject)}?
+ {canBypass && ( + +
+ { + setBypassGovernance(!bypassGovernance); + }} + description="" + /> +
+
+ )}
To continue please type YES, PROCEED in the box. diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/DeleteObject.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/DeleteObject.tsx index a364f007a..4925cb18c 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/DeleteObject.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/DeleteObject.tsx @@ -25,7 +25,10 @@ import { ConfirmDeleteIcon } from "../../../../../../icons"; import FormSwitchWrapper from "../../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper"; import { setErrorSnackMessage } from "../../../../../../systemSlice"; -import { useAppDispatch } from "../../../../../../store"; +import { AppState, useAppDispatch } from "../../../../../../store"; +import { hasPermission } from "../../../../../../common/SecureComponent"; +import { IAM_SCOPES } from "../../../../../../common/SecureComponent/permissions"; +import { useSelector } from "react-redux"; interface IDeleteObjectProps { closeDeleteModalAndRefresh: (refresh: boolean) => void; @@ -42,7 +45,6 @@ const DeleteObject = ({ deleteOpen, selectedBucket, selectedObject, - versioning, selectedVersion = "", }: IDeleteObjectProps) => { @@ -54,6 +56,17 @@ const DeleteObject = ({ const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError); const [deleteVersions, setDeleteVersions] = useState(false); + const [bypassGovernance, setBypassGovernance] = useState(false); + + const retentionConfig = useSelector( + (state: AppState) => state.objectBrowser.retentionConfig + ); + + const canBypass = + hasPermission( + [selectedBucket], + [IAM_SCOPES.S3_BYPASS_GOVERNANCE_RETENTION] + ) && retentionConfig?.mode === "governance"; if (!selectedObject) { return null; @@ -67,7 +80,7 @@ const DeleteObject = ({ selectedVersion !== "" ? `&version_id=${selectedVersion}` : `&recursive=${recursive}&all_versions=${deleteVersions}` - }` + }${bypassGovernance ? "&bypass=true" : ""}` ); }; @@ -115,26 +128,48 @@ const DeleteObject = ({ }} description="" /> - {deleteVersions && ( - -
- This will remove the object as well as all of its versions,{" "} -
- This action is irreversible. -
-
- Are you sure you want to continue? -
- )} + + )} + {canBypass && (deleteVersions || selectedVersion !== "") && ( + +
+ { + setBypassGovernance(!bypassGovernance); + }} + description="" + /> +
+
+ )} + {deleteVersions && ( + +
+ This will remove the object as well as all of its versions,{" "} +
+ This action is irreversible. +
+
+ Are you sure you want to continue?
)}
diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx index bef3db9dd..f7c335225 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx @@ -50,7 +50,10 @@ import { Badge } from "@mui/material"; import BrowserBreadcrumbs from "../../../../ObjectBrowser/BrowserBreadcrumbs"; import { extensionPreview } from "../utils"; import { BucketInfo, BucketQuota } from "../../../types"; -import { ErrorResponseHandler } from "../../../../../../common/types"; +import { + ErrorResponseHandler, + IRetentionConfig, +} from "../../../../../../common/types"; import ScreenTitle from "../../../../Common/ScreenTitle/ScreenTitle"; @@ -103,7 +106,9 @@ import { setNewObject, setObjectDetailsView, setPreviewOpen, + setRetentionConfig, setSearchObjects, + setSelectedBucket, setSelectedObjects, setSelectedObjectView, setSelectedPreview, @@ -273,6 +278,9 @@ const ListObjects = () => { const previewOpen = useSelector( (state: AppState) => state.objectBrowser.previewOpen ); + const selectedBucket = useSelector( + (state: AppState) => state.objectBrowser.selectedBucket + ); const loadingBucket = useSelector(selBucketDetailsLoading); const bucketInfo = useSelector(selBucketDetailsInfo); @@ -411,6 +419,7 @@ const ListObjects = () => { .then((res: BucketInfo) => { dispatch(setBucketDetailsLoad(false)); dispatch(setBucketInfo(res)); + dispatch(setSelectedBucket(bucketName)); }) .catch((err: ErrorResponseHandler) => { dispatch(setBucketDetailsLoad(false)); @@ -419,6 +428,21 @@ const ListObjects = () => { } }, [bucketName, loadingBucket, dispatch]); + // Load retention Config + + useEffect(() => { + if (selectedBucket !== "") { + api + .invoke("GET", `/api/v1/buckets/${selectedBucket}/retention`) + .then((res: IRetentionConfig) => { + dispatch(setRetentionConfig(res)); + }) + .catch((err: ErrorResponseHandler) => { + dispatch(setRetentionConfig(null)); + }); + } + }, [selectedBucket, dispatch]); + const closeDeleteMultipleModalAndRefresh = (refresh: boolean) => { setDeleteMultipleOpen(false); diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/DeleteSelectedVersions.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/DeleteSelectedVersions.tsx index 2b2662da2..6116a6839 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/DeleteSelectedVersions.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/DeleteSelectedVersions.tsx @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { useEffect, useState } from "react"; +import React, { Fragment, useEffect, useState } from "react"; import { DialogContentText } from "@mui/material"; @@ -23,7 +23,11 @@ import ConfirmDialog from "../../../../Common/ModalWrapper/ConfirmDialog"; import { ConfirmDeleteIcon } from "../../../../../../icons"; import api from "../../../../../../common/api"; import { setErrorSnackMessage } from "../../../../../../systemSlice"; -import { useAppDispatch } from "../../../../../../store"; +import { AppState, useAppDispatch } from "../../../../../../store"; +import FormSwitchWrapper from "../../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper"; +import { hasPermission } from "../../../../../../common/SecureComponent"; +import { IAM_SCOPES } from "../../../../../../common/SecureComponent/permissions"; +import { useSelector } from "react-redux"; interface IDeleteSelectedVersionsProps { closeDeleteModalAndRefresh: (refresh: boolean) => void; @@ -42,6 +46,17 @@ const DeleteObject = ({ }: IDeleteSelectedVersionsProps) => { const dispatch = useAppDispatch(); const [deleteLoading, setDeleteLoading] = useState(false); + const [bypassGovernance, setBypassGovernance] = useState(false); + + const retentionConfig = useSelector( + (state: AppState) => state.objectBrowser.retentionConfig + ); + + const canBypass = + hasPermission( + [selectedBucket], + [IAM_SCOPES.S3_BYPASS_GOVERNANCE_RETENTION] + ) && retentionConfig?.mode === "governance"; const onClose = () => closeDeleteModalAndRefresh(false); const onConfirmDelete = () => { @@ -62,7 +77,9 @@ const DeleteObject = ({ api .invoke( "POST", - `/api/v1/buckets/${selectedBucket}/delete-objects?all_versions=false`, + `/api/v1/buckets/${selectedBucket}/delete-objects?all_versions=false${ + bypassGovernance ? "&bypass=true" : "" + }`, selectedObjectsRequest ) .then(() => { @@ -81,6 +98,7 @@ const DeleteObject = ({ selectedBucket, selectedObject, selectedVersions, + bypassGovernance, dispatch, ]); @@ -101,6 +119,28 @@ const DeleteObject = ({ Are you sure you want to delete the selected {selectedVersions.length}{" "} versions for {selectedObject}? + {canBypass && ( + +
+ { + setBypassGovernance(!bypassGovernance); + }} + description="" + /> +
+
+ )}
} /> diff --git a/portal-ui/src/screens/Console/ObjectBrowser/objectBrowserSlice.ts b/portal-ui/src/screens/Console/ObjectBrowser/objectBrowserSlice.ts index 9d5bd385c..26e1b4724 100644 --- a/portal-ui/src/screens/Console/ObjectBrowser/objectBrowserSlice.ts +++ b/portal-ui/src/screens/Console/ObjectBrowser/objectBrowserSlice.ts @@ -20,6 +20,7 @@ import { BucketObjectItem, IRestoreLocalObjectList, } from "../Buckets/ListBuckets/Objects/ListObjects/types"; +import { IRetentionConfig } from "../../../common/types"; const defaultRewind = { rewindEnabled: false, @@ -28,6 +29,7 @@ const defaultRewind = { }; const initialState: ObjectBrowserState = { + selectedBucket: "", versionsMode: false, loadingObjects: true, objectDetailsOpen: false, @@ -64,6 +66,11 @@ const initialState: ObjectBrowserState = { previewOpen: false, shareFileModalOpen: false, isOpeningObjectDetail: false, + retentionConfig: { + mode: "", + unit: "", + validity: 0, + }, }; export const objectBrowserSlice = createSlice({ @@ -340,6 +347,15 @@ export const objectBrowserSlice = createSlice({ setIsOpeningOD: (state, action: PayloadAction) => { state.isOpeningObjectDetail = action.payload; }, + setRetentionConfig: ( + state, + action: PayloadAction + ) => { + state.retentionConfig = action.payload; + }, + setSelectedBucket: (state, action: PayloadAction) => { + state.selectedBucket = action.payload; + }, }, }); export const { @@ -383,6 +399,8 @@ export const { setLoadingRecords, restoreLocalObjectList, setIsOpeningOD, + setRetentionConfig, + setSelectedBucket, } = objectBrowserSlice.actions; export default objectBrowserSlice.reducer; diff --git a/portal-ui/src/screens/Console/ObjectBrowser/types.ts b/portal-ui/src/screens/Console/ObjectBrowser/types.ts index 986052ca3..dfbd49958 100644 --- a/portal-ui/src/screens/Console/ObjectBrowser/types.ts +++ b/portal-ui/src/screens/Console/ObjectBrowser/types.ts @@ -15,6 +15,7 @@ // along with this program. If not, see . import { BucketObjectItem } from "../Buckets/ListBuckets/Objects/ListObjects/types"; +import { IRetentionConfig } from "../../../common/types"; export const REWIND_SET_ENABLE = "REWIND/SET_ENABLE"; export const REWIND_RESET_REWIND = "REWIND/RESET_REWIND"; @@ -64,6 +65,7 @@ export interface RewindItem { } export interface ObjectBrowserState { + selectedBucket: string; rewind: RewindItem; objectManager: ObjectManager; searchObjects: string; @@ -90,6 +92,7 @@ export interface ObjectBrowserState { previewOpen: boolean; shareFileModalOpen: boolean; isOpeningObjectDetail: boolean; + retentionConfig: IRetentionConfig | null; } export interface ObjectManager { diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index 301a03311..473a05b3f 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -1006,6 +1006,11 @@ func init() { "name": "all_versions", "in": "query" }, + { + "type": "boolean", + "name": "bypass", + "in": "query" + }, { "name": "files", "in": "body", @@ -1539,6 +1544,11 @@ func init() { "type": "boolean", "name": "non_current_versions", "in": "query" + }, + { + "type": "boolean", + "name": "bypass", + "in": "query" } ], "responses": { @@ -9438,6 +9448,11 @@ func init() { "name": "all_versions", "in": "query" }, + { + "type": "boolean", + "name": "bypass", + "in": "query" + }, { "name": "files", "in": "body", @@ -9971,6 +9986,11 @@ func init() { "type": "boolean", "name": "non_current_versions", "in": "query" + }, + { + "type": "boolean", + "name": "bypass", + "in": "query" } ], "responses": { diff --git a/restapi/operations/object/delete_multiple_objects_parameters.go b/restapi/operations/object/delete_multiple_objects_parameters.go index f4058449c..5dde532c0 100644 --- a/restapi/operations/object/delete_multiple_objects_parameters.go +++ b/restapi/operations/object/delete_multiple_objects_parameters.go @@ -61,6 +61,10 @@ type DeleteMultipleObjectsParams struct { In: path */ BucketName string + /* + In: query + */ + Bypass *bool /* Required: true In: body @@ -89,6 +93,11 @@ func (o *DeleteMultipleObjectsParams) BindRequest(r *http.Request, route *middle res = append(res, err) } + qBypass, qhkBypass, _ := qs.GetOK("bypass") + if err := o.bindBypass(qBypass, qhkBypass, route.Formats); err != nil { + res = append(res, err) + } + if runtime.HasBody(r) { defer r.Body.Close() var body []*models.DeleteFile @@ -160,3 +169,26 @@ func (o *DeleteMultipleObjectsParams) bindBucketName(rawData []string, hasKey bo return nil } + +// bindBypass binds and validates parameter Bypass from query. +func (o *DeleteMultipleObjectsParams) bindBypass(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("bypass", "query", "bool", raw) + } + o.Bypass = &value + + return nil +} diff --git a/restapi/operations/object/delete_multiple_objects_urlbuilder.go b/restapi/operations/object/delete_multiple_objects_urlbuilder.go index 82e0d3d7c..dfbaade7a 100644 --- a/restapi/operations/object/delete_multiple_objects_urlbuilder.go +++ b/restapi/operations/object/delete_multiple_objects_urlbuilder.go @@ -36,6 +36,7 @@ type DeleteMultipleObjectsURL struct { BucketName string AllVersions *bool + Bypass *bool _basePath string // avoid unkeyed usage @@ -86,6 +87,14 @@ func (o *DeleteMultipleObjectsURL) Build() (*url.URL, error) { qs.Set("all_versions", allVersionsQ) } + var bypassQ string + if o.Bypass != nil { + bypassQ = swag.FormatBool(*o.Bypass) + } + if bypassQ != "" { + qs.Set("bypass", bypassQ) + } + _result.RawQuery = qs.Encode() return &_result, nil diff --git a/restapi/operations/object/delete_object_parameters.go b/restapi/operations/object/delete_object_parameters.go index 2ed955761..6c77771f2 100644 --- a/restapi/operations/object/delete_object_parameters.go +++ b/restapi/operations/object/delete_object_parameters.go @@ -62,6 +62,10 @@ type DeleteObjectParams struct { /* In: query */ + Bypass *bool + /* + In: query + */ NonCurrentVersions *bool /* Required: true @@ -99,6 +103,11 @@ func (o *DeleteObjectParams) BindRequest(r *http.Request, route *middleware.Matc res = append(res, err) } + qBypass, qhkBypass, _ := qs.GetOK("bypass") + if err := o.bindBypass(qBypass, qhkBypass, route.Formats); err != nil { + res = append(res, err) + } + qNonCurrentVersions, qhkNonCurrentVersions, _ := qs.GetOK("non_current_versions") if err := o.bindNonCurrentVersions(qNonCurrentVersions, qhkNonCurrentVersions, route.Formats); err != nil { res = append(res, err) @@ -161,6 +170,29 @@ func (o *DeleteObjectParams) bindBucketName(rawData []string, hasKey bool, forma return nil } +// bindBypass binds and validates parameter Bypass from query. +func (o *DeleteObjectParams) bindBypass(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: false + // AllowEmptyValue: false + + if raw == "" { // empty values pass all other validations + return nil + } + + value, err := swag.ConvertBool(raw) + if err != nil { + return errors.InvalidType("bypass", "query", "bool", raw) + } + o.Bypass = &value + + return nil +} + // bindNonCurrentVersions binds and validates parameter NonCurrentVersions from query. func (o *DeleteObjectParams) bindNonCurrentVersions(rawData []string, hasKey bool, formats strfmt.Registry) error { var raw string diff --git a/restapi/operations/object/delete_object_urlbuilder.go b/restapi/operations/object/delete_object_urlbuilder.go index 915a65e31..a82b2a236 100644 --- a/restapi/operations/object/delete_object_urlbuilder.go +++ b/restapi/operations/object/delete_object_urlbuilder.go @@ -36,6 +36,7 @@ type DeleteObjectURL struct { BucketName string AllVersions *bool + Bypass *bool NonCurrentVersions *bool Path string Recursive *bool @@ -90,6 +91,14 @@ func (o *DeleteObjectURL) Build() (*url.URL, error) { qs.Set("all_versions", allVersionsQ) } + var bypassQ string + if o.Bypass != nil { + bypassQ = swag.FormatBool(*o.Bypass) + } + if bypassQ != "" { + qs.Set("bypass", bypassQ) + } + var nonCurrentVersionsQ string if o.NonCurrentVersions != nil { nonCurrentVersionsQ = swag.FormatBool(*o.NonCurrentVersions) diff --git a/restapi/user_objects.go b/restapi/user_objects.go index 8670af125..c4d160d9d 100644 --- a/restapi/user_objects.go +++ b/restapi/user_objects.go @@ -579,7 +579,7 @@ func getDownloadFolderResponse(session *models.Principal, params objectApi.Downl }), nil } -// getDeleteObjectResponse returns whether there was an errors on deletion of object +// getDeleteObjectResponse returns whether there was an error on deletion of object func getDeleteObjectResponse(session *models.Principal, params objectApi.DeleteObjectParams) *models.Error { ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() @@ -603,6 +603,7 @@ func getDeleteObjectResponse(session *models.Principal, params objectApi.DeleteO var version string var allVersions bool var nonCurrentVersions bool + var bypass bool if params.Recursive != nil { rec = *params.Recursive } @@ -615,28 +616,35 @@ func getDeleteObjectResponse(session *models.Principal, params objectApi.DeleteO if params.NonCurrentVersions != nil { nonCurrentVersions = *params.NonCurrentVersions } + if params.Bypass != nil { + bypass = *params.Bypass + } if allVersions && nonCurrentVersions { err := errors.New("cannot set delete all versions and delete non-current versions flags at the same time") return ErrorWithContext(ctx, err) } - err = deleteObjects(ctx, mcClient, params.BucketName, prefix, version, rec, allVersions, nonCurrentVersions) + err = deleteObjects(ctx, mcClient, params.BucketName, prefix, version, rec, allVersions, nonCurrentVersions, bypass) if err != nil { return ErrorWithContext(ctx, err) } return nil } -// getDeleteMultiplePathsResponse returns whether there was an errors on deletion of any object +// getDeleteMultiplePathsResponse returns whether there was an error on deletion of any object func getDeleteMultiplePathsResponse(session *models.Principal, params objectApi.DeleteMultipleObjectsParams) *models.Error { ctx, cancel := context.WithCancel(params.HTTPRequest.Context()) defer cancel() var version string var allVersions bool + var bypass bool if params.AllVersions != nil { allVersions = *params.AllVersions } + if params.Bypass != nil { + bypass = *params.Bypass + } for i := 0; i < len(params.Files); i++ { if params.Files[i].VersionID != "" { version = params.Files[i].VersionID @@ -649,7 +657,7 @@ func getDeleteMultiplePathsResponse(session *models.Principal, params objectApi. // create a mc S3Client interface implementation // defining the client to be used mcClient := mcClient{client: s3Client} - err = deleteObjects(ctx, mcClient, params.BucketName, params.Files[i].Path, version, params.Files[i].Recursive, allVersions, false) + err = deleteObjects(ctx, mcClient, params.BucketName, params.Files[i].Path, version, params.Files[i].Recursive, allVersions, false, bypass) if err != nil { return ErrorWithContext(ctx, err) } @@ -658,30 +666,29 @@ func getDeleteMultiplePathsResponse(session *models.Principal, params objectApi. } // deleteObjects deletes either a single object or multiple objects based on recursive flag -func deleteObjects(ctx context.Context, client MCClient, bucket string, path string, versionID string, recursive bool, allVersions bool, nonCurrentVersionsOnly bool) error { +func deleteObjects(ctx context.Context, client MCClient, bucket string, path string, versionID string, recursive, allVersions, nonCurrentVersionsOnly, bypass bool) error { // Delete All non-Current versions only. if nonCurrentVersionsOnly { - return deleteNonCurrentVersions(ctx, client, bucket, path) + return deleteNonCurrentVersions(ctx, client, bucket, path, bypass) } if recursive || allVersions { - return deleteMultipleObjects(ctx, client, recursive, allVersions) + return deleteMultipleObjects(ctx, client, recursive, allVersions, bypass) } - return deleteSingleObject(ctx, client, bucket, path, versionID) + return deleteSingleObject(ctx, client, bucket, path, versionID, bypass) } // 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 bool) error { - isBypass := false +func deleteMultipleObjects(ctx context.Context, client MCClient, recursive, allVersions, isBypass bool) error { isIncomplete := false isRemoveBucket := false forceDelete := false - if recursive || allVersions { + if recursive || (allVersions && !isBypass) { forceDelete = true } @@ -722,13 +729,12 @@ func deleteMultipleObjects(ctx context.Context, client MCClient, recursive, allV return nil } -func deleteSingleObject(ctx context.Context, client MCClient, bucket, object string, versionID string) error { +func deleteSingleObject(ctx context.Context, client MCClient, bucket, object string, versionID string, isBypass bool) error { targetURL := fmt.Sprintf("%s/%s", bucket, object) contentCh := make(chan *mc.ClientContent, 1) contentCh <- &mc.ClientContent{URL: *newClientURL(targetURL), VersionID: versionID} close(contentCh) - isBypass := false isIncomplete := false isRemoveBucket := false @@ -741,7 +747,7 @@ func deleteSingleObject(ctx context.Context, client MCClient, bucket, object str return nil } -func deleteNonCurrentVersions(ctx context.Context, client MCClient, bucket, path string) error { +func deleteNonCurrentVersions(ctx context.Context, client MCClient, bucket, path string, isBypass bool) error { lctx, cancel := context.WithCancel(ctx) defer cancel() @@ -773,7 +779,7 @@ func deleteNonCurrentVersions(ctx context.Context, client MCClient, bucket, path } }() - for result := range client.remove(ctx, false, false, false, false, contentCh) { + for result := range client.remove(ctx, false, false, isBypass, false, contentCh) { if result.Err != nil { return result.Err.Cause } diff --git a/restapi/user_objects_test.go b/restapi/user_objects_test.go index 333f17829..26263febf 100644 --- a/restapi/user_objects_test.go +++ b/restapi/user_objects_test.go @@ -734,7 +734,7 @@ func Test_deleteObjects(t *testing.T) { t.Run(tt.test, func(t *testing.T) { mcListMock = tt.args.listFunc mcRemoveMock = tt.args.removeFunc - err := deleteObjects(ctx, s3Client1, tt.args.bucket, tt.args.path, tt.args.versionID, tt.args.recursive, false, tt.args.nonCurrent) + err := deleteObjects(ctx, s3Client1, tt.args.bucket, tt.args.path, tt.args.versionID, tt.args.recursive, false, tt.args.nonCurrent, false) switch { case err == nil && tt.wantError != nil: t.Errorf("deleteObjects() error: %v, wantErr: %v", err, tt.wantError) diff --git a/swagger-console.yml b/swagger-console.yml index 87bdafc15..e0806e449 100644 --- a/swagger-console.yml +++ b/swagger-console.yml @@ -351,6 +351,10 @@ paths: in: query required: false type: boolean + - name: bypass + in: query + required: false + type: boolean responses: 200: description: A successful response. @@ -374,6 +378,10 @@ paths: in: query required: false type: boolean + - name: bypass + in: query + required: false + type: boolean - name: files in: body required: true