Added delete governance bypass option when deleting file versions (#2536)

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2023-01-03 12:35:37 -06:00
committed by GitHub
parent 25e486ef18
commit 255e3d14d0
15 changed files with 364 additions and 49 deletions

View File

@@ -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<boolean>(false);
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
const [deleteVersions, setDeleteVersions] = useState<boolean>(false);
const [bypassGovernance, setBypassGovernance] = useState<boolean>(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 && (
<Fragment>
<div
style={{
marginTop: 10,
}}
>
<FormSwitchWrapper
label={"Bypass Governance Mode"}
indicatorLabels={["Yes", "No"]}
checked={bypassGovernance}
value={"bypass_governance"}
id="bypass_governance"
name="bypass_governance"
onChange={(e) => {
setBypassGovernance(!bypassGovernance);
}}
description=""
/>
</div>
</Fragment>
)}
{deleteVersions && (
<Fragment>
<div

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
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<boolean>(false);
const [typeConfirm, setTypeConfirm] = useState<string>("");
const [bypassGovernance, setBypassGovernance] = useState<boolean>(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 = ({
<DialogContentText>
Are you sure you want to delete all the non-current versions for:{" "}
<b>{decodeURLString(selectedObject)}</b>? <br />
{canBypass && (
<Fragment>
<div
style={{
marginTop: 10,
}}
>
<FormSwitchWrapper
label={"Bypass Governance Mode"}
indicatorLabels={["Yes", "No"]}
checked={bypassGovernance}
value={"bypass_governance"}
id="bypass_governance"
name="bypass_governance"
onChange={(e) => {
setBypassGovernance(!bypassGovernance);
}}
description=""
/>
</div>
</Fragment>
)}
<br />
To continue please type <b>YES, PROCEED</b> in the box.
<Grid item xs={12}>

View File

@@ -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<boolean>(false);
const [bypassGovernance, setBypassGovernance] = useState<boolean>(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 && (
<Fragment>
<div
style={{
marginTop: 10,
border: "#c83b51 1px solid",
borderRadius: 3,
padding: 5,
backgroundColor: "#c83b5120",
color: "#c83b51",
}}
>
This will remove the object as well as all of its versions,{" "}
<br />
This action is irreversible.
</div>
<br />
Are you sure you want to continue?
</Fragment>
)}
</Fragment>
)}
{canBypass && (deleteVersions || selectedVersion !== "") && (
<Fragment>
<div
style={{
marginTop: 10,
}}
>
<FormSwitchWrapper
label={"Bypass Governance Mode"}
indicatorLabels={["Yes", "No"]}
checked={bypassGovernance}
value={"bypass_governance"}
id="bypass_governance"
name="bypass_governance"
onChange={(e) => {
setBypassGovernance(!bypassGovernance);
}}
description=""
/>
</div>
</Fragment>
)}
{deleteVersions && (
<Fragment>
<div
style={{
marginTop: 10,
border: "#c83b51 1px solid",
borderRadius: 3,
padding: 5,
backgroundColor: "#c83b5120",
color: "#c83b51",
}}
>
This will remove the object as well as all of its versions,{" "}
<br />
This action is irreversible.
</div>
<br />
Are you sure you want to continue?
</Fragment>
)}
</DialogContentText>

View File

@@ -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);

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
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<boolean>(false);
const [bypassGovernance, setBypassGovernance] = useState<boolean>(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 = ({
<DialogContentText>
Are you sure you want to delete the selected {selectedVersions.length}{" "}
versions for <strong>{selectedObject}</strong>?
{canBypass && (
<Fragment>
<div
style={{
marginTop: 10,
}}
>
<FormSwitchWrapper
label={"Bypass Governance Mode"}
indicatorLabels={["Yes", "No"]}
checked={bypassGovernance}
value={"bypass_governance"}
id="bypass_governance"
name="bypass_governance"
onChange={(e) => {
setBypassGovernance(!bypassGovernance);
}}
description=""
/>
</div>
</Fragment>
)}
</DialogContentText>
}
/>

View File

@@ -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<boolean>) => {
state.isOpeningObjectDetail = action.payload;
},
setRetentionConfig: (
state,
action: PayloadAction<IRetentionConfig | null>
) => {
state.retentionConfig = action.payload;
},
setSelectedBucket: (state, action: PayloadAction<string>) => {
state.selectedBucket = action.payload;
},
},
});
export const {
@@ -383,6 +399,8 @@ export const {
setLoadingRecords,
restoreLocalObjectList,
setIsOpeningOD,
setRetentionConfig,
setSelectedBucket,
} = objectBrowserSlice.actions;
export default objectBrowserSlice.reducer;

View File

@@ -15,6 +15,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
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 {

View File

@@ -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": {

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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