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 b7e19a8e4..0e53080a6 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 @@ -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 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(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 = ({ Are you sure you want to delete the selected {selectedObjects.length}{" "} objects?{" "} + {versioning && ( + { + setDeleteVersions(!deleteVersions); + }} + description="" + /> + )} } /> 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 80926a83b..60ff63943 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 @@ -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 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(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={ Are you sure you want to delete:{" "} - {decodeFileName(selectedObject)}?{" "} + {decodeFileName(selectedObject)}?
+ {versioning && ( + { + setDeleteVersions(!deleteVersions); + }} + description="" + /> + )}
} /> 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 c396f2547..595391eed 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 @@ -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 && ( diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx index 24d071bd4..c4c27b3c4 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx @@ -478,6 +478,7 @@ const ObjectDetails = ({ selectedBucket={bucketName} selectedObject={internalPaths} closeDeleteModalAndRefresh={closeDeleteModal} + versioning={distributedSetup} /> )} {tagModalOpen && actualInfo && ( diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index efec1d6a4..fd374e482 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -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": { diff --git a/restapi/operations/user_api/delete_multiple_objects_parameters.go b/restapi/operations/user_api/delete_multiple_objects_parameters.go index 580fd8fcc..c11b88c6c 100644 --- a/restapi/operations/user_api/delete_multiple_objects_parameters.go +++ b/restapi/operations/user_api/delete_multiple_objects_parameters.go @@ -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 diff --git a/restapi/operations/user_api/delete_multiple_objects_urlbuilder.go b/restapi/operations/user_api/delete_multiple_objects_urlbuilder.go index a0ecb133f..7894e5775 100644 --- a/restapi/operations/user_api/delete_multiple_objects_urlbuilder.go +++ b/restapi/operations/user_api/delete_multiple_objects_urlbuilder.go @@ -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 } diff --git a/restapi/operations/user_api/delete_object_parameters.go b/restapi/operations/user_api/delete_object_parameters.go index 627429fce..0ada360e0 100644 --- a/restapi/operations/user_api/delete_object_parameters.go +++ b/restapi/operations/user_api/delete_object_parameters.go @@ -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 diff --git a/restapi/operations/user_api/delete_object_urlbuilder.go b/restapi/operations/user_api/delete_object_urlbuilder.go index 0b68f179a..1773c8705 100644 --- a/restapi/operations/user_api/delete_object_urlbuilder.go +++ b/restapi/operations/user_api/delete_object_urlbuilder.go @@ -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) diff --git a/restapi/user_objects.go b/restapi/user_objects.go index 80806fbda..9218e2249 100644 --- a/restapi/user_objects.go +++ b/restapi/user_objects.go @@ -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) diff --git a/restapi/user_objects_test.go b/restapi/user_objects_test.go index fed10b987..bd20116d5 100644 --- a/restapi/user_objects_test.go +++ b/restapi/user_objects_test.go @@ -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 diff --git a/swagger-console.yml b/swagger-console.yml index 5019d0eb9..c2cda1970 100644 --- a/swagger-console.yml +++ b/swagger-console.yml @@ -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