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:
@@ -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>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -478,6 +478,7 @@ const ObjectDetails = ({
|
||||
selectedBucket={bucketName}
|
||||
selectedObject={internalPaths}
|
||||
closeDeleteModalAndRefresh={closeDeleteModal}
|
||||
versioning={distributedSetup}
|
||||
/>
|
||||
)}
|
||||
{tagModalOpen && actualInfo && (
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user