Delete Non-current versions (#1735)

- Delete Non-current API
- Delete non current modal implementation
Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2022-03-18 13:07:34 -07:00
committed by GitHub
parent d7fef8d89e
commit 5ab5232474
11 changed files with 366 additions and 24 deletions

View File

@@ -0,0 +1,37 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 <http://www.gnu.org/licenses/>.
import * as React from "react";
import { SVGProps } from "react";
const DeleteNonCurrentIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={"min-icon"}
viewBox="0 0 256 256"
fill={"currentcolor"}
{...props}
>
<path
d="M222.83,0H114.08a5.38,5.38,0,0,0-5.38,5.37V118.1c.62.39,1.24.79,1.85,1.2a74.53,74.53,0,0,1,22.09,100.36h90.19a5.36,5.36,0,0,0,5.37-5.37V5.37A5.37,5.37,0,0,0,222.83,0Z"
/>
<path
d="M106,125.38a68,68,0,1,0,30,56.35A67.59,67.59,0,0,0,106,125.38Zm8.16,94.78-7.77,7.76L68,189.5,29.56,227.92l-7.77-7.76,38.42-38.43L21.79,143.31l7.77-7.77L68,174l38.42-38.42,7.77,7.77L75.75,181.73Z"
/>
</svg>
);
export default DeleteNonCurrentIcon;

View File

@@ -180,3 +180,4 @@ export { default as ArrowRightLink } from "./ArrowRightLink";
export { default as LicenseDocIcon } from "./LicenseDocIcon";
export { default as SelectAllIcon } from "./SelectAllIcon";
export { default as BackIcon } from "./BackIcon";
export { default as DeleteNonCurrentIcon } from "./DeleteNonCurrentIcon";

View File

@@ -0,0 +1,118 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 <http://www.gnu.org/licenses/>.
import React, { useState, useEffect } from "react";
import { connect } from "react-redux";
import { DialogContentText } from "@mui/material";
import Grid from "@mui/material/Grid";
import { setErrorSnackMessage } from "../../../../../../actions";
import { ErrorResponseHandler } from "../../../../../../common/types";
import { decodeFileName } from "../../../../../../common/utils";
import { ConfirmDeleteIcon } from "../../../../../../icons";
import ConfirmDialog from "../../../../Common/ModalWrapper/ConfirmDialog";
import api from "../../../../../../common/api";
import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
interface IDeleteNonCurrentProps {
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedObject: string;
selectedBucket: string;
setErrorSnackMessage: typeof setErrorSnackMessage;
}
const DeleteNonCurrentVersions = ({
closeDeleteModalAndRefresh,
deleteOpen,
selectedBucket,
selectedObject,
setErrorSnackMessage,
}: IDeleteNonCurrentProps) => {
const [deleteLoading, setDeleteLoading] = useState<boolean>(false);
const [typeConfirm, setTypeConfirm] = useState<string>("");
useEffect(() => {
if (deleteLoading) {
api
.invoke(
"DELETE",
`/api/v1/buckets/${selectedBucket}/objects?path=${selectedObject}&non_current_versions=true`
)
.then(() => {
closeDeleteModalAndRefresh(true);
})
.catch((error: ErrorResponseHandler) => {
setErrorSnackMessage(error);
setDeleteLoading(false);
});
}
}, [
deleteLoading,
closeDeleteModalAndRefresh,
setErrorSnackMessage,
selectedObject,
selectedBucket,
]);
if (!selectedObject) {
return null;
}
const onConfirmDelete = () => {
setDeleteLoading(true);
};
return (
<ConfirmDialog
title={`Delete Non-Current versions`}
confirmText={"Delete"}
isOpen={deleteOpen}
titleIcon={<ConfirmDeleteIcon />}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={() => closeDeleteModalAndRefresh(false)}
confirmButtonProps={{
disabled: typeConfirm !== "YES, PROCEED" || deleteLoading,
}}
confirmationContent={
<DialogContentText>
Are you sure you want to delete all the non-current versions for:{" "}
<b>{decodeFileName(selectedObject)}</b>? <br />
<br />
To continue please type <b>YES, PROCEED</b> in the box.
<Grid item xs={12}>
<InputBoxWrapper
id="type-confirm"
name="retype-tenant"
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setTypeConfirm(event.target.value);
}}
label=""
value={typeConfirm}
/>
</Grid>
</DialogContentText>
}
/>
);
};
const mapDispatchToProps = {
setErrorSnackMessage,
};
const connector = connect(null, mapDispatchToProps);
export default connector(DeleteNonCurrentVersions);

View File

@@ -55,11 +55,13 @@ import {
} from "../../../../ObjectBrowser/actions";
import { AppState } from "../../../../../../store";
import { VersionsIcon } from "../../../../../../icons";
import { DeleteNonCurrentIcon, VersionsIcon } from "../../../../../../icons";
import VirtualizedList from "../../../../Common/VirtualizedList/VirtualizedList";
import FileVersionItem from "./FileVersionItem";
import SelectWrapper from "../../../../Common/FormComponents/SelectWrapper/SelectWrapper";
import PreviewFileModal from "../Preview/PreviewFileModal";
import RBIconButton from "../../../BucketDetails/SummaryItems/RBIconButton";
import DeleteNonCurrent from "../ListObjects/DeleteNonCurrent";
const styles = (theme: Theme) =>
createStyles({
@@ -160,6 +162,8 @@ const VersionsNavigator = ({
const [restoreVersion, setRestoreVersion] = useState<string>("");
const [sortValue, setSortValue] = useState<string>("date");
const [previewOpen, setPreviewOpen] = useState<boolean>(false);
const [deleteNonCurrentOpen, setDeleteNonCurrentOpen] =
useState<boolean>(false);
// calculate object name to display
let objectNameArray: string[] = [];
@@ -283,6 +287,16 @@ const VersionsNavigator = ({
}
};
const closeDeleteNonCurrent = (reloadAfterDelete: boolean) => {
setDeleteNonCurrentOpen(false);
if (reloadAfterDelete) {
setLoadingVersions(true);
setSelectedVersion("");
setLoadingObjectInfo(true);
}
};
const totalSpace = versions.reduce((acc: number, currValue: IFileInfo) => {
if (currValue.size) {
return acc + parseInt(currValue.size);
@@ -376,6 +390,14 @@ const VersionsNavigator = ({
}}
/>
)}
{deleteNonCurrentOpen && (
<DeleteNonCurrent
deleteOpen={deleteNonCurrentOpen}
closeDeleteModalAndRefresh={closeDeleteNonCurrent}
selectedBucket={bucketName}
selectedObject={internalPaths}
/>
)}
<Grid container className={classes.versionsContainer}>
{!actualInfo && (
<Grid item xs={12}>
@@ -417,6 +439,18 @@ const VersionsNavigator = ({
}
actions={
<Fragment>
<RBIconButton
id={"delete-non-current"}
tooltip={"Delete Non Current Versions"}
onClick={() => {
setDeleteNonCurrentOpen(true);
}}
text={""}
icon={<DeleteNonCurrentIcon />}
color="secondary"
style={{ marginRight: 15 }}
disabled={versions.length <= 1}
/>
<span className={classes.sortByLabel}>Sort by</span>
<SelectWrapper
id={"sort-by"}

View File

@@ -343,6 +343,12 @@ const IconsScreen = ({ classes }: IIconsScreenSimple) => {
DeleteIcon
</Grid>
<Grid item xs={3} sm={2} md={1}>
<cicons.DeleteNonCurrentIcon />
<br />
DeleteNonCurrentIcon
</Grid>
<Grid item xs={3} sm={2} md={1}>
<cicons.DiagnosticsFeatureIcon />
<br />

View File

@@ -1315,6 +1315,11 @@ func init() {
"type": "boolean",
"name": "all_versions",
"in": "query"
},
{
"type": "boolean",
"name": "non_current_versions",
"in": "query"
}
],
"responses": {
@@ -7799,6 +7804,11 @@ func init() {
"type": "boolean",
"name": "all_versions",
"in": "query"
},
{
"type": "boolean",
"name": "non_current_versions",
"in": "query"
}
],
"responses": {

View File

@@ -59,6 +59,10 @@ type DeleteObjectParams struct {
In: path
*/
BucketName string
/*
In: query
*/
NonCurrentVersions *bool
/*
Required: true
In: query
@@ -95,6 +99,11 @@ func (o *DeleteObjectParams) BindRequest(r *http.Request, route *middleware.Matc
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)
}
qPath, qhkPath, _ := qs.GetOK("path")
if err := o.bindPath(qPath, qhkPath, route.Formats); err != nil {
res = append(res, err)
@@ -152,6 +161,29 @@ func (o *DeleteObjectParams) bindBucketName(rawData []string, hasKey bool, forma
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
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("non_current_versions", "query", "bool", raw)
}
o.NonCurrentVersions = &value
return nil
}
// bindPath binds and validates parameter Path from query.
func (o *DeleteObjectParams) bindPath(rawData []string, hasKey bool, formats strfmt.Registry) error {
if !hasKey {

View File

@@ -35,10 +35,11 @@ import (
type DeleteObjectURL struct {
BucketName string
AllVersions *bool
Path string
Recursive *bool
VersionID *string
AllVersions *bool
NonCurrentVersions *bool
Path string
Recursive *bool
VersionID *string
_basePath string
// avoid unkeyed usage
@@ -89,6 +90,14 @@ func (o *DeleteObjectURL) Build() (*url.URL, error) {
qs.Set("all_versions", allVersionsQ)
}
var nonCurrentVersionsQ string
if o.NonCurrentVersions != nil {
nonCurrentVersionsQ = swag.FormatBool(*o.NonCurrentVersions)
}
if nonCurrentVersionsQ != "" {
qs.Set("non_current_versions", nonCurrentVersionsQ)
}
pathQ := o.Path
if pathQ != "" {
qs.Set("path", pathQ)

View File

@@ -570,6 +570,7 @@ func getDeleteObjectResponse(session *models.Principal, params user_api.DeleteOb
var rec bool
var version string
var allVersions bool
var nonCurrentVersions bool
if params.Recursive != nil {
rec = *params.Recursive
}
@@ -579,7 +580,16 @@ func getDeleteObjectResponse(session *models.Principal, params user_api.DeleteOb
if params.AllVersions != nil {
allVersions = *params.AllVersions
}
err = deleteObjects(ctx, mcClient, params.BucketName, prefix, version, rec, allVersions)
if params.NonCurrentVersions != nil {
nonCurrentVersions = *params.NonCurrentVersions
}
if allVersions && nonCurrentVersions {
err := errors.New("cannot set delete all versions and delete non-current versions flags at the same time")
return prepareError(err)
}
err = deleteObjects(ctx, mcClient, params.BucketName, prefix, version, rec, allVersions, nonCurrentVersions)
if err != nil {
return prepareError(err)
}
@@ -606,7 +616,7 @@ func getDeleteMultiplePathsResponse(session *models.Principal, params user_api.D
// 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)
err = deleteObjects(ctx, mcClient, params.BucketName, params.Files[i].Path, version, params.Files[i].Recursive, allVersions, false)
if err != nil {
return prepareError(err)
}
@@ -615,7 +625,15 @@ func getDeleteMultiplePathsResponse(session *models.Principal, params user_api.D
}
// 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) error {
func deleteObjects(ctx context.Context, client MCClient, bucket string, path string, versionID string, recursive bool, allVersions bool, nonCurrentVersionsOnly bool) error {
// Delete All non-Current versions only.
if nonCurrentVersionsOnly {
if err := deleteNonCurrentVersions(ctx, client, bucket, path); err != nil {
return err
}
return nil
}
if allVersions {
if err := deleteMultipleObjects(ctx, client, recursive, true); err != nil {
return err
@@ -718,6 +736,25 @@ func deleteSingleObject(ctx context.Context, client MCClient, bucket, object str
return nil
}
func deleteNonCurrentVersions(ctx context.Context, client MCClient, bucket, path string) error {
// Get current object versions
for lsObj := range client.list(ctx, mc.ListOptions{WithDeleteMarkers: true, WithOlderVersions: true, Recursive: true}) {
if lsObj.Err != nil {
return errors.New(lsObj.Err.String())
}
if !lsObj.IsLatest {
err := deleteSingleObject(ctx, client, bucket, path, lsObj.VersionID)
if err != nil {
return err
}
}
}
return nil
}
func getUploadObjectResponse(session *models.Principal, params user_api.PostBucketsBucketNameObjectsUploadParams) *models.Error {
ctx := context.Background()
mClient, err := newMinioClient(session)

View File

@@ -583,6 +583,7 @@ func Test_deleteObjects(t *testing.T) {
path string
versionID string
recursive bool
nonCurrent bool
listFunc func(ctx context.Context, opts mc.ListOptions) <-chan *mc.ClientContent
removeFunc func(ctx context.Context, isIncomplete, isRemoveBucket, isBypass bool, contentCh <-chan *mc.ClientContent) <-chan mc.RemoveResult
}
@@ -594,9 +595,10 @@ func Test_deleteObjects(t *testing.T) {
{
test: "Remove single object",
args: args{
path: "obj.txt",
versionID: "",
recursive: false,
path: "obj.txt",
versionID: "",
recursive: false,
nonCurrent: false,
removeFunc: func(ctx context.Context, isIncomplete, isRemoveBucket, isBypass bool, contentCh <-chan *mc.ClientContent) <-chan mc.RemoveResult {
resultCh := make(chan mc.RemoveResult, 1)
resultCh <- mc.RemoveResult{Err: nil}
@@ -609,9 +611,10 @@ func Test_deleteObjects(t *testing.T) {
{
test: "Error on Remove single object",
args: args{
path: "obj.txt",
versionID: "",
recursive: false,
path: "obj.txt",
versionID: "",
recursive: false,
nonCurrent: false,
removeFunc: func(ctx context.Context, isIncomplete, isRemoveBucket, isBypass bool, contentCh <-chan *mc.ClientContent) <-chan mc.RemoveResult {
resultCh := make(chan mc.RemoveResult, 1)
resultCh <- mc.RemoveResult{Err: probe.NewError(errors.New("probe error"))}
@@ -624,9 +627,10 @@ func Test_deleteObjects(t *testing.T) {
{
test: "Remove multiple objects",
args: args{
path: "path/",
versionID: "",
recursive: true,
path: "path/",
versionID: "",
recursive: true,
nonCurrent: false,
removeFunc: func(ctx context.Context, isIncomplete, isRemoveBucket, isBypass bool, contentCh <-chan *mc.ClientContent) <-chan mc.RemoveResult {
resultCh := make(chan mc.RemoveResult, 1)
resultCh <- mc.RemoveResult{Err: nil}
@@ -647,9 +651,10 @@ func Test_deleteObjects(t *testing.T) {
// while deleting multiple objects
test: "Error on Remove multiple objects 1",
args: args{
path: "path/",
versionID: "",
recursive: true,
path: "path/",
versionID: "",
recursive: true,
nonCurrent: false,
removeFunc: func(ctx context.Context, isIncomplete, isRemoveBucket, isBypass bool, contentCh <-chan *mc.ClientContent) <-chan mc.RemoveResult {
resultCh := make(chan mc.RemoveResult, 1)
resultCh <- mc.RemoveResult{Err: nil}
@@ -670,9 +675,58 @@ func Test_deleteObjects(t *testing.T) {
// while deleting multiple objects
test: "Error on Remove multiple objects 2",
args: args{
path: "path/",
versionID: "",
recursive: true,
path: "path/",
versionID: "",
recursive: true,
nonCurrent: false,
removeFunc: func(ctx context.Context, isIncomplete, isRemoveBucket, isBypass bool, contentCh <-chan *mc.ClientContent) <-chan mc.RemoveResult {
resultCh := make(chan mc.RemoveResult, 1)
resultCh <- mc.RemoveResult{Err: probe.NewError(errors.New("probe error"))}
close(resultCh)
return resultCh
},
listFunc: func(ctx context.Context, opts mc.ListOptions) <-chan *mc.ClientContent {
ch := make(chan *mc.ClientContent, 1)
ch <- &mc.ClientContent{}
close(ch)
return ch
},
},
wantError: errors.New("probe error"),
},
{
// Description handle error when error happens on remove function
// while deleting multiple objects
test: "Remove non current objects",
args: args{
path: "path/",
versionID: "",
recursive: true,
nonCurrent: true,
removeFunc: func(ctx context.Context, isIncomplete, isRemoveBucket, isBypass bool, contentCh <-chan *mc.ClientContent) <-chan mc.RemoveResult {
resultCh := make(chan mc.RemoveResult, 1)
resultCh <- mc.RemoveResult{Err: nil}
close(resultCh)
return resultCh
},
listFunc: func(ctx context.Context, opts mc.ListOptions) <-chan *mc.ClientContent {
ch := make(chan *mc.ClientContent, 1)
ch <- &mc.ClientContent{}
close(ch)
return ch
},
},
wantError: nil,
},
{
// Description handle error when error happens on remove function
// while deleting multiple objects
test: "Error deleting non current objects",
args: args{
path: "path/",
versionID: "",
recursive: true,
nonCurrent: true,
removeFunc: func(ctx context.Context, isIncomplete, isRemoveBucket, isBypass bool, contentCh <-chan *mc.ClientContent) <-chan mc.RemoveResult {
resultCh := make(chan mc.RemoveResult, 1)
resultCh <- mc.RemoveResult{Err: probe.NewError(errors.New("probe error"))}
@@ -695,7 +749,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)
err := deleteObjects(ctx, s3Client1, tt.args.bucket, tt.args.path, tt.args.versionID, tt.args.recursive, false, tt.args.nonCurrent)
if err == nil && tt.wantError != nil {
t.Errorf("deleteObjects() error: %v, wantErr: %v", err, tt.wantError)
} else if err != nil && tt.wantError == nil {

View File

@@ -341,6 +341,10 @@ paths:
in: query
required: false
type: boolean
- name: non_current_versions
in: query
required: false
type: boolean
responses:
200:
description: A successful response.