Delete all versions (#1376)

* delete all versions

* style

Co-authored-by: Prakash Senthil Vel <23444145+prakashsvmx@users.noreply.github.com>
Co-authored-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
adfost
2022-01-20 17:28:52 -08:00
committed by GitHub
parent d9531f9617
commit 3ba7b34b25
12 changed files with 211 additions and 16 deletions

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 from "react";
import React, { useState } from "react";
import { connect } from "react-redux";
import { DialogContentText } from "@mui/material";
import { setErrorSnackMessage } from "../../../../../../actions";
@@ -22,6 +22,7 @@ import { ErrorResponseHandler } from "../../../../../../common/types";
import useApi from "../../../../Common/Hooks/useApi";
import ConfirmDialog from "../../../../Common/ModalWrapper/ConfirmDialog";
import { ConfirmDeleteIcon } from "../../../../../../icons";
import FormSwitchWrapper from "../../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
interface IDeleteObjectProps {
closeDeleteModalAndRefresh: (refresh: boolean) => void;
@@ -29,6 +30,7 @@ interface IDeleteObjectProps {
selectedObjects: string[];
selectedBucket: string;
setErrorSnackMessage: typeof setErrorSnackMessage;
versioning: boolean;
}
const DeleteObject = ({
@@ -37,10 +39,12 @@ const DeleteObject = ({
selectedBucket,
selectedObjects,
setErrorSnackMessage,
versioning,
}: IDeleteObjectProps) => {
const onDelSuccess = () => closeDeleteModalAndRefresh(true);
const onDelError = (err: ErrorResponseHandler) => setErrorSnackMessage(err);
const onClose = () => closeDeleteModalAndRefresh(false);
const [deleteVersions, setDeleteVersions] = useState<boolean>(false);
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
@@ -68,7 +72,7 @@ const DeleteObject = ({
if (toSend) {
invokeDeleteApi(
"POST",
`/api/v1/buckets/${selectedBucket}/delete-objects`,
`/api/v1/buckets/${selectedBucket}/delete-objects?all_versions=${deleteVersions}`,
toSend
);
}
@@ -87,6 +91,20 @@ const DeleteObject = ({
<DialogContentText>
Are you sure you want to delete the selected {selectedObjects.length}{" "}
objects?{" "}
{versioning && (
<FormSwitchWrapper
label={"Delete All Versions"}
indicatorLabels={["Yes", "No"]}
checked={deleteVersions}
value={"delete_versions"}
id="delete-versions"
name="delete-versions"
onChange={(e) => {
setDeleteVersions(!deleteVersions);
}}
description=""
/>
)}
</DialogContentText>
}
/>

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 from "react";
import React, { useState } from "react";
import { connect } from "react-redux";
import { DialogContentText } from "@mui/material";
import { setErrorSnackMessage } from "../../../../../../actions";
@@ -23,6 +23,7 @@ import { decodeFileName } from "../../../../../../common/utils";
import ConfirmDialog from "../../../../Common/ModalWrapper/ConfirmDialog";
import useApi from "../../../../Common/Hooks/useApi";
import { ConfirmDeleteIcon } from "../../../../../../icons";
import FormSwitchWrapper from "../../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
interface IDeleteObjectProps {
closeDeleteModalAndRefresh: (refresh: boolean) => void;
@@ -30,6 +31,7 @@ interface IDeleteObjectProps {
selectedObject: string;
selectedBucket: string;
setErrorSnackMessage: typeof setErrorSnackMessage;
versioning: boolean;
}
const DeleteObject = ({
@@ -38,12 +40,14 @@ const DeleteObject = ({
selectedBucket,
selectedObject,
setErrorSnackMessage,
versioning,
}: IDeleteObjectProps) => {
const onDelSuccess = () => closeDeleteModalAndRefresh(true);
const onDelError = (err: ErrorResponseHandler) => setErrorSnackMessage(err);
const onClose = () => closeDeleteModalAndRefresh(false);
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
const [deleteVersions, setDeleteVersions] = useState<boolean>(false);
if (!selectedObject) {
return null;
@@ -53,7 +57,7 @@ const DeleteObject = ({
const recursive = decodedSelectedObject.endsWith("/");
invokeDeleteApi(
"DELETE",
`/api/v1/buckets/${selectedBucket}/objects?path=${selectedObject}&recursive=${recursive}`
`/api/v1/buckets/${selectedBucket}/objects?path=${selectedObject}&recursive=${recursive}&all_versions=${deleteVersions}`
);
};
@@ -69,7 +73,21 @@ const DeleteObject = ({
confirmationContent={
<DialogContentText>
Are you sure you want to delete:{" "}
<b>{decodeFileName(selectedObject)}</b>?{" "}
<b>{decodeFileName(selectedObject)}</b>? <br />
{versioning && (
<FormSwitchWrapper
label={"Delete All Versions"}
indicatorLabels={["Yes", "No"]}
checked={deleteVersions}
value={"delete_versions"}
id="delete-versions"
name="delete-versions"
onChange={(e) => {
setDeleteVersions(!deleteVersions);
}}
description=""
/>
)}
</DialogContentText>
}
/>

View File

@@ -1098,6 +1098,7 @@ const ListObjects = ({
selectedBucket={bucketName}
selectedObject={encodeFileName(selectedObject)}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
versioning={isVersioned}
/>
)}
{deleteMultipleOpen && (
@@ -1106,6 +1107,7 @@ const ListObjects = ({
selectedBucket={bucketName}
selectedObjects={selectedObjects}
closeDeleteModalAndRefresh={closeDeleteMultipleModalAndRefresh}
versioning={isVersioned}
/>
)}
{createFolderOpen && (

View File

@@ -478,6 +478,7 @@ const ObjectDetails = ({
selectedBucket={bucketName}
selectedObject={internalPaths}
closeDeleteModalAndRefresh={closeDeleteModal}
versioning={distributedSetup}
/>
)}
{tagModalOpen && actualInfo && (

View File

@@ -731,6 +731,11 @@ func init() {
"in": "path",
"required": true
},
{
"type": "boolean",
"name": "all_versions",
"in": "query"
},
{
"name": "files",
"in": "body",
@@ -1186,6 +1191,11 @@ func init() {
"type": "boolean",
"name": "recursive",
"in": "query"
},
{
"type": "boolean",
"name": "all_versions",
"in": "query"
}
],
"responses": {
@@ -6570,6 +6580,11 @@ func init() {
"in": "path",
"required": true
},
{
"type": "boolean",
"name": "all_versions",
"in": "query"
},
{
"name": "files",
"in": "body",
@@ -7025,6 +7040,11 @@ func init() {
"type": "boolean",
"name": "recursive",
"in": "query"
},
{
"type": "boolean",
"name": "all_versions",
"in": "query"
}
],
"responses": {

View File

@@ -30,6 +30,7 @@ import (
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/minio/console/models"
)
@@ -51,6 +52,10 @@ type DeleteMultipleObjectsParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
In: query
*/
AllVersions *bool
/*
Required: true
In: path
@@ -72,6 +77,13 @@ func (o *DeleteMultipleObjectsParams) BindRequest(r *http.Request, route *middle
o.HTTPRequest = r
qs := runtime.Values(r.URL.Query())
qAllVersions, qhkAllVersions, _ := qs.GetOK("all_versions")
if err := o.bindAllVersions(qAllVersions, qhkAllVersions, route.Formats); err != nil {
res = append(res, err)
}
rBucketName, rhkBucketName, _ := route.Params.GetOK("bucket_name")
if err := o.bindBucketName(rBucketName, rhkBucketName, route.Formats); err != nil {
res = append(res, err)
@@ -112,6 +124,29 @@ func (o *DeleteMultipleObjectsParams) BindRequest(r *http.Request, route *middle
return nil
}
// bindAllVersions binds and validates parameter AllVersions from query.
func (o *DeleteMultipleObjectsParams) bindAllVersions(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("all_versions", "query", "bool", raw)
}
o.AllVersions = &value
return nil
}
// bindBucketName binds and validates parameter BucketName from path.
func (o *DeleteMultipleObjectsParams) bindBucketName(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string

View File

@@ -27,12 +27,16 @@ import (
"net/url"
golangswaggerpaths "path"
"strings"
"github.com/go-openapi/swag"
)
// DeleteMultipleObjectsURL generates an URL for the delete multiple objects operation
type DeleteMultipleObjectsURL struct {
BucketName string
AllVersions *bool
_basePath string
// avoid unkeyed usage
_ struct{}
@@ -72,6 +76,18 @@ func (o *DeleteMultipleObjectsURL) Build() (*url.URL, error) {
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
qs := make(url.Values)
var allVersionsQ string
if o.AllVersions != nil {
allVersionsQ = swag.FormatBool(*o.AllVersions)
}
if allVersionsQ != "" {
qs.Set("all_versions", allVersionsQ)
}
_result.RawQuery = qs.Encode()
return &_result, nil
}

View File

@@ -50,6 +50,10 @@ type DeleteObjectParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
In: query
*/
AllVersions *bool
/*
Required: true
In: path
@@ -81,6 +85,11 @@ func (o *DeleteObjectParams) BindRequest(r *http.Request, route *middleware.Matc
qs := runtime.Values(r.URL.Query())
qAllVersions, qhkAllVersions, _ := qs.GetOK("all_versions")
if err := o.bindAllVersions(qAllVersions, qhkAllVersions, route.Formats); err != nil {
res = append(res, err)
}
rBucketName, rhkBucketName, _ := route.Params.GetOK("bucket_name")
if err := o.bindBucketName(rBucketName, rhkBucketName, route.Formats); err != nil {
res = append(res, err)
@@ -106,6 +115,29 @@ func (o *DeleteObjectParams) BindRequest(r *http.Request, route *middleware.Matc
return nil
}
// bindAllVersions binds and validates parameter AllVersions from query.
func (o *DeleteObjectParams) bindAllVersions(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("all_versions", "query", "bool", raw)
}
o.AllVersions = &value
return nil
}
// bindBucketName binds and validates parameter BucketName from path.
func (o *DeleteObjectParams) bindBucketName(rawData []string, hasKey bool, formats strfmt.Registry) error {
var raw string

View File

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

View File

@@ -562,13 +562,22 @@ func getDeleteObjectResponse(session *models.Principal, params user_api.DeleteOb
mcClient := mcClient{client: s3Client}
var rec bool
var version string
var allVersions bool
if params.Recursive != nil {
rec = *params.Recursive
}
if params.VersionID != nil {
version = *params.VersionID
}
err = deleteObjects(ctx, mcClient, params.BucketName, prefix, version, rec)
if params.AllVersions != nil {
allVersions = *params.AllVersions
}
minClient, err := newMinioClient(session)
if err != nil {
return prepareError(err)
}
client2 := minioClient{client: minClient}
err = deleteObjects(ctx, mcClient, client2, params.BucketName, prefix, version, rec, allVersions)
if err != nil {
return prepareError(err)
}
@@ -579,6 +588,15 @@ func getDeleteObjectResponse(session *models.Principal, params user_api.DeleteOb
func getDeleteMultiplePathsResponse(session *models.Principal, params user_api.DeleteMultipleObjectsParams) *models.Error {
ctx := context.Background()
var version string
var allVersions bool
if params.AllVersions != nil {
allVersions = *params.AllVersions
}
minClient, err := newMinioClient(session)
if err != nil {
return prepareError(err)
}
client2 := minioClient{client: minClient}
for i := 0; i < len(params.Files); i++ {
if params.Files[i].VersionID != "" {
version = params.Files[i].VersionID
@@ -591,7 +609,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)
err = deleteObjects(ctx, mcClient, client2, params.BucketName, params.Files[i].Path, version, params.Files[i].Recursive, allVersions)
if err != nil {
return prepareError(err)
}
@@ -600,9 +618,26 @@ 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, path string, versionID string, recursive bool) error {
func deleteObjects(ctx context.Context, client MCClient, client2 MinioClient, bucket string, path string, versionID string, recursive bool, allVersions bool) error {
if allVersions {
if recursive {
if err := deleteMultipleObjects(ctx, client, recursive, true); err != nil {
return err
}
} else {
objects, err := listBucketObjects(ctx, client2, bucket, path, recursive, true, false)
if err != nil {
return err
}
for i := range objects {
if err := deleteSingleObject(ctx, client, bucket, path, objects[i].VersionID); err != nil {
return err
}
}
}
}
if recursive {
if err := deleteMultipleObjects(ctx, client, recursive); err != nil {
if err := deleteMultipleObjects(ctx, client, recursive, false); err != nil {
return err
}
} else {
@@ -616,11 +651,11 @@ func deleteObjects(ctx context.Context, client MCClient, bucket, path string, ve
// deleteMultipleObjects uses listing before removal, it can list recursively or not,
// Use cases:
// * Remove objects recursively
func deleteMultipleObjects(ctx context.Context, client MCClient, recursive bool) error {
func deleteMultipleObjects(ctx context.Context, client MCClient, recursive bool, allVersions bool) error {
isRemoveBucket := false
isIncomplete := false
isBypass := false
listOpts := mc.ListOptions{Recursive: recursive, Incomplete: isIncomplete, ShowDir: mc.DirNone}
listOpts := mc.ListOptions{Recursive: recursive, Incomplete: isIncomplete, ShowDir: mc.DirNone, WithOlderVersions: allVersions, WithDeleteMarkers: allVersions}
// TODO: support older Versions
contentCh := make(chan *mc.ClientContent, 1)

View File

@@ -569,7 +569,8 @@ func Test_listObjects(t *testing.T) {
func Test_deleteObjects(t *testing.T) {
ctx := context.Background()
client := s3ClientMock{}
s3Client1 := s3ClientMock{}
minioClient1 := minioClientMock{}
type args struct {
bucket string
path string
@@ -723,7 +724,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, client, tt.args.bucket, tt.args.path, tt.args.versionID, tt.args.recursive)
err := deleteObjects(ctx, s3Client1, minioClient1, tt.args.bucket, tt.args.path, tt.args.versionID, tt.args.recursive, false)
if !reflect.DeepEqual(err, tt.wantError) {
t.Errorf("deleteObjects() error: %v, wantErr: %v", err, tt.wantError)
return

View File

@@ -335,6 +335,10 @@ paths:
in: query
required: false
type: boolean
- name: all_versions
in: query
required: false
type: boolean
responses:
200:
description: A successful response.
@@ -354,6 +358,10 @@ paths:
in: path
required: true
type: string
- name: all_versions
in: query
required: false
type: boolean
- name: files
in: body
required: true