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