Added Exclude Folders & Exclude Prefixes support (#2973)

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2023-08-02 13:35:00 -06:00
committed by GitHub
parent 0d628f589a
commit 49f856bdd5
17 changed files with 433 additions and 194 deletions

View File

@@ -31,7 +31,7 @@ import (
func Test_AddAccessRuleAPI(t *testing.T) {
assert := assert.New(t)
AddBucket("testaccessruleadd", false, false, nil, nil)
AddBucket("testaccessruleadd", false, nil, nil, nil)
type args struct {
bucket string
@@ -111,7 +111,7 @@ func Test_AddAccessRuleAPI(t *testing.T) {
func Test_GetAccessRulesAPI(t *testing.T) {
assert := assert.New(t)
AddBucket("testaccessruleget", false, false, nil, nil)
AddBucket("testaccessruleget", false, nil, nil, nil)
type args struct {
bucket string
@@ -161,7 +161,7 @@ func Test_GetAccessRulesAPI(t *testing.T) {
func Test_DeleteAccessRuleAPI(t *testing.T) {
assert := assert.New(t)
AddBucket("testaccessruledelete", false, false, nil, nil)
AddBucket("testaccessruledelete", false, nil, nil, nil)
type args struct {
prefix string

View File

@@ -42,14 +42,14 @@ import (
type AddBucketOps struct {
Name string
Locking bool
Versioning bool
Versioning map[string]interface{}
Quota map[string]interface{}
Retention map[string]interface{}
Endpoint *string
UseToken *string
}
func AddBucket(name string, locking, versioning bool, quota, retention map[string]interface{}) (*http.Response, error) {
func AddBucket(name string, locking bool, versioning, quota, retention map[string]interface{}) (*http.Response, error) {
return AddBucketWithOpts(&AddBucketOps{
Name: name,
Locking: locking,
@@ -142,11 +142,11 @@ func getTokenForEndpoint(endpoint string) string {
return loginToken
}
func setupBucket(name string, locking, versioning bool, quota, retention map[string]interface{}, assert *assert.Assertions, expected int) bool {
func setupBucket(name string, locking bool, versioning, quota, retention map[string]interface{}, assert *assert.Assertions, expected int) bool {
return setupBucketForEndpoint(name, locking, versioning, quota, retention, assert, expected, nil, nil)
}
func setupBucketForEndpoint(name string, locking, versioning bool, quota, retention map[string]interface{}, assert *assert.Assertions, expected int, endpoint, endpointToken *string) bool {
func setupBucketForEndpoint(name string, locking bool, versioning, quota, retention map[string]interface{}, assert *assert.Assertions, expected int, endpoint, endpointToken *string) bool {
/*
The intention of this function is to return either true or false to
reduce the code by performing the verification in one place only.
@@ -751,7 +751,7 @@ func TestPutObjectsLegalholdStatus(t *testing.T) {
status := "enabled"
// 1. Create bucket
if !setupBucket(bucketName, true, true, nil, nil, assert, 200) {
if !setupBucket(bucketName, true, map[string]interface{}{"enabled": true}, nil, nil, assert, 200) {
return
}
@@ -838,7 +838,7 @@ func TestGetBucketQuota(t *testing.T) {
validBucket := "testgetbucketquota"
// 1. Create bucket
if !setupBucket(validBucket, true, true, nil, nil, assert, 200) {
if !setupBucket(validBucket, true, map[string]interface{}{"enabled": true}, nil, nil, assert, 200) {
return
}
@@ -915,7 +915,7 @@ func TestPutBucketQuota(t *testing.T) {
validBucket := "testputbucketquota"
// 1. Create bucket
if !setupBucket(validBucket, true, true, nil, nil, assert, 200) {
if !setupBucket(validBucket, true, map[string]interface{}{"enabled": true}, nil, nil, assert, 200) {
return
}
@@ -974,7 +974,7 @@ func TestListBucketEvents(t *testing.T) {
validBucket := "testlistbucketevents"
// 1. Create bucket
if !setupBucket(validBucket, true, true, nil, nil, assert, 200) {
if !setupBucket(validBucket, true, map[string]interface{}{"enabled": true}, nil, nil, assert, 200) {
return
}
@@ -1032,7 +1032,7 @@ func TestDeleteObjectsRetentionStatus(t *testing.T) {
validPrefix := encodeBase64(fileName)
// 1. Create bucket
if !setupBucket(bucketName, true, true, nil, nil, assert, 200) {
if !setupBucket(bucketName, true, map[string]interface{}{"enabled": true}, nil, nil, assert, 200) {
return
}
@@ -1139,7 +1139,7 @@ func TestBucketSetPolicy(t *testing.T) {
validBucketName := "testbucketsetpolicy"
// 1. Create bucket
if !setupBucket(validBucketName, true, true, nil, nil, assert, 200) {
if !setupBucket(validBucketName, true, map[string]interface{}{"enabled": true}, nil, nil, assert, 200) {
return
}
@@ -1200,7 +1200,7 @@ func TestRestoreObjectToASelectedVersion(t *testing.T) {
validPrefix := encodeBase64(fileName)
// 1. Create bucket
if !setupBucket(bucketName, true, true, nil, nil, assert, 200) {
if !setupBucket(bucketName, true, map[string]interface{}{"enabled": true}, nil, nil, assert, 200) {
return
}
@@ -1288,7 +1288,7 @@ func TestPutBucketsTags(t *testing.T) {
// 1. Create the bucket
assert := assert.New(t)
validBucketName := "testputbuckettags1"
if !setupBucket(validBucketName, false, false, nil, nil, assert, 200) {
if !setupBucket(validBucketName, false, nil, nil, nil, assert, 200) {
return
}
@@ -1346,7 +1346,7 @@ func TestGetsTheMetadataOfAnObject(t *testing.T) {
tags["tag"] = "testputobjecttagbucketonetagone"
// 1. Create the bucket
if !setupBucket(bucketName, false, false, nil, nil, assert, 200) {
if !setupBucket(bucketName, false, nil, nil, nil, assert, 200) {
return
}
@@ -1417,7 +1417,7 @@ func TestPutObjectsRetentionStatus(t *testing.T) {
prefix := encodeBase64(fileName)
// 1. Create bucket
if !setupBucket(bucketName, true, true, nil, nil, assert, 200) {
if !setupBucket(bucketName, true, map[string]interface{}{"enabled": true}, nil, nil, assert, 200) {
return
}
@@ -1515,7 +1515,7 @@ func TestShareObjectOnURL(t *testing.T) {
versionID := "null"
// 1. Create the bucket
if !setupBucket(bucketName, false, false, nil, nil, assert, 200) {
if !setupBucket(bucketName, false, nil, nil, nil, assert, 200) {
return
}
@@ -1589,7 +1589,7 @@ func TestListObjects(t *testing.T) {
fileName := "testlistobjecttobucket1.txt"
// 1. Create the bucket
if !setupBucket(bucketName, false, false, nil, nil, assert, 200) {
if !setupBucket(bucketName, false, nil, nil, nil, assert, 200) {
return
}
@@ -1637,7 +1637,7 @@ func TestDeleteObject(t *testing.T) {
numberOfFiles := 2
// 1. Create bucket
if !setupBucket(bucketName, true, true, nil, nil, assert, 200) {
if !setupBucket(bucketName, true, map[string]interface{}{"enabled": true}, nil, nil, assert, 200) {
return
}
@@ -1703,7 +1703,7 @@ func TestUploadObjectToBucket(t *testing.T) {
fileName := "sample.txt"
// 1. Create the bucket
if !setupBucket(bucketName, false, false, nil, nil, assert, 200) {
if !setupBucket(bucketName, false, nil, nil, nil, assert, 200) {
return
}
@@ -1738,7 +1738,7 @@ func TestDownloadObject(t *testing.T) {
}
// 1. Create the bucket
if !setupBucket(bucketName, true, true, nil, nil, assert, 200) {
if !setupBucket(bucketName, true, map[string]interface{}{"enabled": true}, nil, nil, assert, 200) {
return
}
@@ -1800,7 +1800,7 @@ func TestDeleteMultipleObjects(t *testing.T) {
fileName := "testdeletemultipleobjs"
// 1. Create a bucket for this particular test
if !setupBucket(bucketName, false, false, nil, nil, assert, 200) {
if !setupBucket(bucketName, false, nil, nil, nil, assert, 200) {
return
}
@@ -1877,7 +1877,7 @@ func TestPutObjectTag(t *testing.T) {
versionID := "null"
// 1. Create the bucket
if !setupBucket(bucketName, false, false, nil, nil, assert, 200) {
if !setupBucket(bucketName, false, nil, nil, nil, assert, 200) {
return
}
@@ -1951,7 +1951,7 @@ func TestBucketRetention(t *testing.T) {
retention["mode"] = "compliance"
retention["unit"] = "years"
retention["validity"] = 2
if !setupBucket("setbucketretention1", true, true, nil, retention, assert, 200) {
if !setupBucket("setbucketretention1", true, map[string]interface{}{"enabled": true}, nil, retention, assert, 200) {
return
}
@@ -2002,7 +2002,7 @@ func TestBucketInformationGenericErrorResponse(t *testing.T) {
// 1. Create the bucket
assert := assert.New(t)
if !setupBucket("bucketinformation2", false, false, nil, nil, assert, 200) {
if !setupBucket("bucketinformation2", false, nil, nil, nil, assert, 200) {
return
}
@@ -2047,7 +2047,7 @@ func TestBucketInformationSuccessfulResponse(t *testing.T) {
// 1. Create the bucket
assert := assert.New(t)
if !setupBucket("bucketinformation1", false, false, nil, nil, assert, 200) {
if !setupBucket("bucketinformation1", false, nil, nil, nil, assert, 200) {
return
}
@@ -2160,7 +2160,7 @@ func TestListBuckets(t *testing.T) {
// 1. Create buckets
numberOfBuckets := 3
for i := 1; i <= numberOfBuckets; i++ {
if !setupBucket("testlistbuckets"+strconv.Itoa(i), false, false, nil, nil, assert, 200) {
if !setupBucket("testlistbuckets"+strconv.Itoa(i), false, nil, nil, nil, assert, 200) {
return
}
}
@@ -2267,7 +2267,7 @@ func TestBucketVersioning(t *testing.T) {
requestDataBody := bytes.NewReader(requestDataJSON)
if !setupBucket("test2", true, false, nil, nil, assert, 200) {
if !setupBucket("test2", true, nil, nil, nil, assert, 200) {
return
}
@@ -2335,7 +2335,7 @@ func TestSetBucketTags(t *testing.T) {
}
// put bucket
if !setupBucket("test4", false, false, nil, nil, assert, 200) {
if !setupBucket("test4", false, nil, nil, nil, assert, 200) {
return
}
@@ -2402,7 +2402,7 @@ func TestGetBucket(t *testing.T) {
Timeout: 2 * time.Second,
}
if !setupBucket("test3", false, false, nil, nil, assert, 200) {
if !setupBucket("test3", false, nil, nil, nil, assert, 200) {
return
}
@@ -2455,7 +2455,7 @@ func TestAddBucket(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if !setupBucket(tt.args.bucketName, false, false, nil, nil, assert, tt.expectedStatus) {
if !setupBucket(tt.args.bucketName, false, nil, nil, nil, assert, tt.expectedStatus) {
return
}
})
@@ -3000,7 +3000,7 @@ func TestReturnsTheStatusOfObjectLockingSupportOnTheBucket(t *testing.T) {
)
}
func SetBucketVersioning(bucketName string, versioning bool, endpoint, useToken *string) (*http.Response, error) {
func SetBucketVersioning(bucketName string, versioning map[string]interface{}, endpoint, useToken *string) (*http.Response, error) {
/*
Helper function to set Bucket Versioning
*/
@@ -3037,7 +3037,7 @@ func TestSetBucketVersioning(t *testing.T) {
assert := assert.New(t)
bucket := "test-set-bucket-versioning"
locking := false
versioning := true
versioning := map[string]interface{}{"enabled": true}
// 1. Create bucket with versioning as true and locking as false
if !setupBucket(bucket, locking, versioning, nil, nil, assert, 200) {
@@ -3045,7 +3045,7 @@ func TestSetBucketVersioning(t *testing.T) {
}
// 2. Set versioning as False i.e Suspend versioning
response, err := SetBucketVersioning(bucket, false, nil, nil)
response, err := SetBucketVersioning(bucket, map[string]interface{}{"enabled": false}, nil, nil)
assert.Nil(err)
if err != nil {
log.Println(err)
@@ -3118,12 +3118,11 @@ func TestEnableBucketEncryption(t *testing.T) {
assert := assert.New(t)
bucketName := "test-enable-bucket-encryption"
locking := false
versioning := false
encType := "sse-s3"
kmsKeyID := ""
// 1. Add bucket
if !setupBucket(bucketName, locking, versioning, nil, nil, assert, 200) {
if !setupBucket(bucketName, locking, nil, nil, nil, assert, 200) {
return
}
@@ -3381,7 +3380,6 @@ func TestBucketLifeCycle(t *testing.T) {
assert := assert.New(t)
bucketName := "test-bucket-life-cycle"
locking := false
versioning := false
ltype := "expiry"
prefix := ""
tags := ""
@@ -3392,7 +3390,7 @@ func TestBucketLifeCycle(t *testing.T) {
var noncurrentversionExpirationDays int64
// 1. Add bucket
if !setupBucket(bucketName, locking, versioning, nil, nil, assert, 200) {
if !setupBucket(bucketName, locking, nil, nil, nil, assert, 200) {
return
}
@@ -3597,12 +3595,11 @@ func TestAccessRule(t *testing.T) {
assert := assert.New(t)
bucketName := "test-access-rule-bucket"
locking := false
versioning := false
prefix := "prefix"
access := "readonly"
// 1. Add bucket
if !setupBucket(bucketName, locking, versioning, nil, nil, assert, 200) {
if !setupBucket(bucketName, locking, nil, nil, nil, assert, 200) {
return
}
@@ -3846,16 +3843,16 @@ func TestAddRemoteBucket(t *testing.T) {
fmt.Println("targetBucket: ", targetBucket)
// 1. Create bucket
if !setupBucket("source", true, true, nil, nil, assert, 200) {
if !setupBucket("source", true, map[string]interface{}{"enabled": true}, nil, nil, assert, 200) {
return
}
// 1.1. Create target bucket
targetEndpoint := "http://localhost:9092"
targetToken := getTokenForEndpoint(targetEndpoint)
if !setupBucketForEndpoint(targetBucket, true, true, nil, nil, assert, 200, &targetEndpoint, &targetToken) {
if !setupBucketForEndpoint(targetBucket, true, map[string]interface{}{"enabled": true}, nil, nil, assert, 200, &targetEndpoint, &targetToken) {
log.Println("bucket already exists")
}
_, err := SetBucketVersioning(targetBucket, false, &targetURL, &targetToken)
_, err := SetBucketVersioning(targetBucket, map[string]interface{}{"enabled": false}, &targetURL, &targetToken)
if err != nil {
log.Println("bucket already has versioning")
}
@@ -3905,16 +3902,16 @@ func TestDeleteRemoteBucket(t *testing.T) {
fmt.Println("targetBucket: ", targetBucket)
// 1. Create bucket
if !setupBucket("deletesource", true, true, nil, nil, assert, 200) {
if !setupBucket("deletesource", true, map[string]interface{}{"enabled": true}, nil, nil, assert, 200) {
return
}
// 1.1. Create target bucket
targetEndpoint := "http://localhost:9092"
targetToken := getTokenForEndpoint(targetEndpoint)
if !setupBucketForEndpoint(targetBucket, true, true, nil, nil, assert, 200, &targetEndpoint, &targetToken) {
if !setupBucketForEndpoint(targetBucket, true, map[string]interface{}{"enabled": true}, nil, nil, assert, 200, &targetEndpoint, &targetToken) {
log.Println("bucket already exists")
}
_, err := SetBucketVersioning(targetBucket, false, &targetURL, &targetToken)
_, err := SetBucketVersioning(targetBucket, map[string]interface{}{"enabled": false}, &targetURL, &targetToken)
if err != nil {
log.Println("bucket already has versioning")
}

View File

@@ -50,7 +50,7 @@ type MakeBucketRequest struct {
Retention *PutBucketRetentionRequest `json:"retention,omitempty"`
// versioning
Versioning bool `json:"versioning,omitempty"`
Versioning *SetBucketVersioning `json:"versioning,omitempty"`
}
// Validate validates this make bucket request
@@ -69,6 +69,10 @@ func (m *MakeBucketRequest) Validate(formats strfmt.Registry) error {
res = append(res, err)
}
if err := m.validateVersioning(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
@@ -122,6 +126,25 @@ func (m *MakeBucketRequest) validateRetention(formats strfmt.Registry) error {
return nil
}
func (m *MakeBucketRequest) validateVersioning(formats strfmt.Registry) error {
if swag.IsZero(m.Versioning) { // not required
return nil
}
if m.Versioning != nil {
if err := m.Versioning.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("versioning")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("versioning")
}
return err
}
}
return nil
}
// ContextValidate validate this make bucket request based on the context it is used
func (m *MakeBucketRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
@@ -134,6 +157,10 @@ func (m *MakeBucketRequest) ContextValidate(ctx context.Context, formats strfmt.
res = append(res, err)
}
if err := m.contextValidateVersioning(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
@@ -172,6 +199,22 @@ func (m *MakeBucketRequest) contextValidateRetention(ctx context.Context, format
return nil
}
func (m *MakeBucketRequest) contextValidateVersioning(ctx context.Context, formats strfmt.Registry) error {
if m.Versioning != nil {
if err := m.Versioning.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("versioning")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("versioning")
}
return err
}
}
return nil
}
// MarshalBinary interface implementation
func (m *MakeBucketRequest) MarshalBinary() ([]byte, error) {
if m == nil {

View File

@@ -34,8 +34,14 @@ import (
// swagger:model setBucketVersioning
type SetBucketVersioning struct {
// versioning
Versioning bool `json:"versioning,omitempty"`
// enabled
Enabled bool `json:"enabled,omitempty"`
// exclude folders
ExcludeFolders bool `json:"excludeFolders,omitempty"`
// exclude prefixes
ExcludePrefixes []string `json:"excludePrefixes"`
}
// Validate validates this set bucket versioning

View File

@@ -126,7 +126,7 @@ export interface BucketObject {
export interface MakeBucketRequest {
name: string;
locking?: boolean;
versioning?: boolean;
versioning?: SetBucketVersioning;
quota?: SetBucketQuota;
retention?: PutBucketRetentionRequest;
}
@@ -802,7 +802,10 @@ export interface BucketVersioningResponse {
}
export interface SetBucketVersioning {
versioning?: boolean;
enabled?: boolean;
/** @maxLength 10 */
excludePrefixes?: string[];
excludeFolders?: boolean;
}
export interface BucketObLockingResponse {

View File

@@ -407,6 +407,7 @@ const BucketSummary = () => {
modalOpen={enableVersioningOpen}
selectedBucket={bucketName}
versioningInfo={versioningInfo}
objectLockingEnabled={!!hasObjectLocking}
/>
)}
@@ -581,6 +582,7 @@ const BucketSummary = () => {
}
onEdit={setBucketVersioning}
isLoading={loadingVersioning}
disabled={hasObjectLocking}
/>
{versioningInfo?.status === "Enabled" ? (

View File

@@ -15,32 +15,54 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useState } from "react";
import { Box, ConfirmModalIcon } from "mds";
import { Box, Button, FormLayout, ModalBox, Switch } from "mds";
import { BucketVersioningResponse } from "api/consoleApi";
import { api } from "api";
import { errorToHandler } from "api/errors";
import { setErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import VersioningInfo from "../VersioningInfo";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
import CSVMultiSelector from "../../Common/FormComponents/CSVMultiSelector/CSVMultiSelector";
import { modalStyleUtils } from "../../Common/FormComponents/common/styleLibrary";
interface IVersioningEventProps {
closeVersioningModalAndRefresh: (refresh: boolean) => void;
modalOpen: boolean;
selectedBucket: string;
versioningInfo: BucketVersioningResponse | undefined;
objectLockingEnabled: boolean;
}
const parseExcludedPrefixes = (
bucketVersioning: BucketVersioningResponse | undefined,
) => {
const excludedPrefixes = bucketVersioning?.excludedPrefixes;
if (excludedPrefixes) {
return excludedPrefixes.map((item) => item.prefix).join(",");
}
return "";
};
const EnableVersioningModal = ({
closeVersioningModalAndRefresh,
modalOpen,
selectedBucket,
versioningInfo = {},
objectLockingEnabled,
}: IVersioningEventProps) => {
const isVersioningEnabled = versioningInfo.status === "Enabled";
const dispatch = useAppDispatch();
const [versioningLoading, setVersioningLoading] = useState<boolean>(false);
const [versionState, setVersionState] = useState<boolean>(
versioningInfo?.status === "Enabled",
);
const [excludeFolders, setExcludeFolders] = useState<boolean>(
!!versioningInfo?.excludeFolders,
);
const [excludedPrefixes, setExcludedPrefixes] = useState<string>(
parseExcludedPrefixes(versioningInfo),
);
const enableVersioning = () => {
if (versioningLoading) {
@@ -50,7 +72,11 @@ const EnableVersioningModal = ({
api.buckets
.setBucketVersioning(selectedBucket, {
versioning: !isVersioningEnabled,
enabled: versionState,
excludeFolders: versionState ? excludeFolders : false,
excludePrefixes: versionState
? excludedPrefixes.split(",").filter((item) => item.trim() !== "")
: [],
})
.then(() => {
setVersioningLoading(false);
@@ -62,44 +88,76 @@ const EnableVersioningModal = ({
});
};
const resetForm = () => {
setExcludedPrefixes("");
setExcludeFolders(false);
setVersionState(false);
};
return (
<ConfirmDialog
<ModalBox
onClose={() => closeVersioningModalAndRefresh(false)}
open={modalOpen}
title={`Versioning on Bucket`}
confirmText={isVersioningEnabled ? "Suspend" : "Enable"}
isOpen={modalOpen}
isLoading={versioningLoading}
titleIcon={<ConfirmModalIcon />}
onConfirm={enableVersioning}
confirmButtonProps={{
variant: "callAction",
}}
onClose={() => {
closeVersioningModalAndRefresh(false);
}}
confirmationContent={
<Box id="alert-dialog-description">
Are you sure you want to{" "}
<strong>{isVersioningEnabled ? "suspend" : "enable"}</strong>{" "}
versioning for this bucket?
{isVersioningEnabled && (
<Fragment>
<br />
<br />
<strong>File versions won't be automatically deleted.</strong>
</Fragment>
)}
<Box
sx={{
paddingTop: "20px",
}}
>
{isVersioningEnabled ? (
<VersioningInfo versioningState={versioningInfo} />
) : null}
</Box>
>
<FormLayout withBorders={false} containerPadding={false}>
<Switch
id={"activateVersioning"}
label={"Versioning Status"}
checked={versionState}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setVersionState(e.target.checked);
}}
indicatorLabels={["Enabled", "Disabled"]}
/>
{versionState && !objectLockingEnabled && (
<Fragment>
<Switch
id={"excludeFolders"}
label={"Exclude Folders"}
checked={excludeFolders}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setExcludeFolders(e.target.checked);
}}
indicatorLabels={["Enabled", "Disabled"]}
/>
<CSVMultiSelector
elements={excludedPrefixes}
label={"Excluded Prefixes"}
name={"excludedPrefixes"}
onChange={(value: string | string[]) => {
let valCh = "";
if (Array.isArray(value)) {
valCh = value.join(",");
} else {
valCh = value;
}
setExcludedPrefixes(valCh);
}}
withBorder={true}
/>
</Fragment>
)}
<Box sx={modalStyleUtils.modalButtonBar}>
<Button
id={"clear"}
type="button"
variant="regular"
color="primary"
onClick={resetForm}
label={"Clear"}
/>
<Button
type="submit"
variant="callAction"
onClick={enableVersioning}
id="saveTag"
label={"Save"}
/>
</Box>
}
/>
</FormLayout>
</ModalBox>
);
};

View File

@@ -15,7 +15,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Box, ValuePair, ActionLink } from "mds";
import { ActionLink, Box, ValuePair } from "mds";
import { SecureComponent } from "../../../../../common/SecureComponent";
import EditActionButton from "./EditActionButton";
@@ -28,6 +28,7 @@ type EditablePropertyItemProps = {
value: any;
onEdit: () => void;
secureCmpProps?: Record<any, any>;
disabled?: boolean;
};
const SecureAction = ({
@@ -61,6 +62,7 @@ const EditablePropertyItem = ({
property = null,
value = null,
onEdit,
disabled = false,
}: EditablePropertyItemProps) => {
return (
<Box
@@ -84,6 +86,7 @@ const EditablePropertyItem = ({
onClick={onEdit}
label={value}
sx={{ fontWeight: "bold" }}
disabled={disabled}
/>
</SecureAction>
}
@@ -104,6 +107,7 @@ const EditablePropertyItem = ({
height: "16px",
},
}}
disabled={disabled}
/>
</SecureAction>
</Box>

View File

@@ -48,6 +48,8 @@ import SectionTitle from "../../../Common/SectionTitle";
import {
resetForm,
setEnableObjectLocking,
setExcludedPrefixes,
setExcludeFolders,
setIsDirty,
setName,
setQuota,
@@ -77,6 +79,7 @@ import {
} from "../../../../../api/consoleApi";
import { errorToHandler } from "../../../../../api/errors";
import HelpMenu from "../../../HelpMenu";
import CSVMultiSelector from "../../../Common/FormComponents/CSVMultiSelector/CSVMultiSelector";
const ErrorBox = styled.div(({ theme }) => ({
color: get(theme, "signalColors.danger", "#C51B3F"),
@@ -102,6 +105,12 @@ const AddBucket = () => {
const versioningEnabled = useSelector(
(state: AppState) => state.addBucket.versioningEnabled,
);
const excludeFolders = useSelector(
(state: AppState) => state.addBucket.excludeFolders,
);
const excludedPrefixes = useSelector(
(state: AppState) => state.addBucket.excludedPrefixes,
);
const lockingEnabled = useSelector(
(state: AppState) => state.addBucket.lockingEnabled,
);
@@ -346,6 +355,35 @@ const AddBucket = () => {
)
}
/>
{versioningEnabled && distributedSetup && !lockingEnabled && (
<Fragment>
<Switch
id={"excludeFolders"}
label={"Exclude Folders"}
checked={excludeFolders}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setExcludeFolders(e.target.checked));
}}
indicatorLabels={["Enabled", "Disabled"]}
/>
<CSVMultiSelector
elements={excludedPrefixes}
label={"Excluded Prefixes"}
name={"excludedPrefixes"}
onChange={(value: string | string[]) => {
let valCh = "";
if (Array.isArray(value)) {
valCh = value.join(",");
} else {
valCh = value;
}
dispatch(setExcludedPrefixes(valCh));
}}
withBorder={true}
/>
</Fragment>
)}
<Switch
value="locking"
id="locking"
@@ -363,7 +401,11 @@ const AddBucket = () => {
label={"Object Locking"}
tooltip={
lockingAllowed
? ""
? `${
versioningEnabled
? "Exclude Folders & Exclude Prefixes options will not be available if this option is enabled."
: ""
}`
: permissionTooltipHelper(
[
IAM_SCOPES.S3_PUT_BUCKET_VERSIONING,

View File

@@ -41,13 +41,25 @@ export const addBucketAsync = createAsyncThunk(
const retentionValidity = state.addBucket.retentionValidity;
const distributedSetup = state.system.distributedSetup;
const siteReplicationInfo = state.system.siteReplicationInfo;
const excludeFolders = state.addBucket.excludeFolders;
const excludedPrefixes = state.addBucket.excludedPrefixes;
let request: MakeBucketRequest = {
name: bucketName,
versioning:
distributedSetup && !siteReplicationInfo.enabled
? versioningEnabled
: false,
versioning: {
enabled:
distributedSetup && !siteReplicationInfo.enabled
? versioningEnabled
: false,
excludePrefixes:
distributedSetup && !siteReplicationInfo.enabled && !lockingEnabled
? excludedPrefixes.split(",").filter((item) => item.trim() !== "")
: [],
excludeFolders:
distributedSetup && !siteReplicationInfo.enabled && !lockingEnabled
? excludeFolders
: false,
},
locking: distributedSetup ? lockingEnabled : false,
};

View File

@@ -34,6 +34,8 @@ export interface AddBucketState {
retentionUnit: string;
retentionValidity: number;
navigateTo: string;
excludeFolders: boolean;
excludedPrefixes: string;
}
const initialState: AddBucketState = {
@@ -52,6 +54,8 @@ const initialState: AddBucketState = {
retentionUnit: "days",
retentionValidity: 180,
navigateTo: "",
excludeFolders: false,
excludedPrefixes: "",
};
export const addBucketsSlice = createSlice({
@@ -81,6 +85,12 @@ export const addBucketsSlice = createSlice({
state.retentionValidity = 180;
}
},
setExcludeFolders: (state, action: PayloadAction<boolean>) => {
state.excludeFolders = action.payload;
},
setExcludedPrefixes: (state, action: PayloadAction<string>) => {
state.excludedPrefixes = action.payload;
},
setEnableObjectLocking: (state, action: PayloadAction<boolean>) => {
state.lockingEnabled = action.payload;
},
@@ -196,6 +206,8 @@ export const {
setRetentionMode,
setRetentionUnit,
setRetentionValidity,
setExcludedPrefixes,
setExcludeFolders,
} = addBucketsSlice.actions;
export default addBucketsSlice.reducer;

View File

@@ -20,16 +20,10 @@ import React, {
useEffect,
useRef,
useState,
Fragment,
} from "react";
import get from "lodash/get";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import Grid from "@mui/material/Grid";
import { InputLabel, Tooltip } from "@mui/material";
import { fieldBasic, tooltipHelper } from "../common/styleLibrary";
import { AddIcon, HelpIcon } from "mds";
import InputBoxWrapper from "../InputBoxWrapper/InputBoxWrapper";
import { AddIcon, Box, HelpIcon, InputBox, InputLabel, Tooltip } from "mds";
interface ICSVMultiSelector {
elements: string;
@@ -37,37 +31,10 @@ interface ICSVMultiSelector {
label: string;
tooltip?: string;
commonPlaceholder?: string;
classes: any;
withBorder?: boolean;
onChange: (elements: string) => void;
}
const styles = (theme: Theme) => {
return createStyles({
...fieldBasic,
...tooltipHelper,
inputWithBorder: {
border: "1px solid #EAEAEA",
padding: 15,
height: 150,
overflowY: "auto",
position: "relative",
marginTop: 15,
flex: 1,
},
inputBoxSpacer: {
marginBottom: 7,
},
inputLabel: {
...fieldBasic.inputLabel,
margin: 0,
alignItems: "flex-start",
paddingTop: "20px",
minWidth: 162,
},
});
};
const CSVMultiSelector = ({
elements,
name,
@@ -76,7 +43,6 @@ const CSVMultiSelector = ({
commonPlaceholder = "",
onChange,
withBorder = false,
classes,
}: ICSVMultiSelector) => {
const [currentElements, setCurrentElements] = useState<string[]>([""]);
const bottomList = createRef<HTMLDivElement>();
@@ -154,55 +120,65 @@ const CSVMultiSelector = ({
const inputs = currentElements.map((element, index) => {
return (
<div
className={classes.inputBoxSpacer}
<InputBox
key={`csv-multi-${name}-${index.toString()}`}
>
<InputBoxWrapper
id={`${name}-${index.toString()}`}
label={""}
name={`${name}-${index.toString()}`}
value={currentElements[index]}
onChange={onChangeElement}
index={index}
key={`csv-${name}-${index.toString()}`}
placeholder={commonPlaceholder}
overlayIcon={
index === currentElements.length - 1 ? <AddIcon /> : null
}
overlayAction={() => {
addEmptyLine(currentElements);
}}
/>
</div>
id={`${name}-${index.toString()}`}
label={""}
name={`${name}-${index.toString()}`}
value={currentElements[index]}
onChange={onChangeElement}
index={index}
placeholder={commonPlaceholder}
overlayIcon={index === currentElements.length - 1 ? <AddIcon /> : null}
overlayAction={() => {
addEmptyLine(currentElements);
}}
/>
);
});
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
<InputLabel className={classes.inputLabel}>
<Fragment>
<Box sx={{ display: "flex" }} className={"inputItem"}>
<InputLabel
sx={{
alignItems: "flex-start",
}}
>
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<div className={classes.tooltip}>
<Box
sx={{
marginLeft: 5,
display: "flex",
alignItems: "center",
"& .min-icon": {
width: 13,
},
}}
>
<Tooltip tooltip={tooltip} placement="top">
<Box className={tooltip}>
<HelpIcon />
</div>
</Box>
</Tooltip>
</div>
</Box>
)}
</InputLabel>
<Grid
item
xs={12}
className={`${withBorder ? classes.inputWithBorder : ""}`}
<Box
withBorders={withBorder}
sx={{
width: "100%",
overflowY: "auto",
height: 150,
position: "relative",
}}
>
{inputs}
<div ref={bottomList} />
</Grid>
</Grid>
</React.Fragment>
</Box>
</Box>
</Fragment>
);
};
export default withStyles(styles)(CSVMultiSelector);
export default CSVMultiSelector;

View File

@@ -232,7 +232,7 @@ type MCClient interface {
list(ctx context.Context, opts mc.ListOptions) <-chan *mc.ClientContent
get(ctx context.Context, opts mc.GetOptions) (io.ReadCloser, *probe.Error)
shareDownload(ctx context.Context, versionID string, expires time.Duration) (string, *probe.Error)
setVersioning(ctx context.Context, status string) *probe.Error
setVersioning(ctx context.Context, status string, excludePrefix []string, excludeFolders bool) *probe.Error
}
// Interface implementation
@@ -265,8 +265,8 @@ func (c mcClient) deleteAllReplicationRules(ctx context.Context) *probe.Error {
return c.client.RemoveReplication(ctx)
}
func (c mcClient) setVersioning(ctx context.Context, status string) *probe.Error {
return c.client.SetVersion(ctx, status, []string{}, false)
func (c mcClient) setVersioning(ctx context.Context, status string, excludePrefix []string, excludeFolders bool) *probe.Error {
return c.client.SetVersion(ctx, status, excludePrefix, excludeFolders)
}
func (c mcClient) remove(ctx context.Context, isIncomplete, isRemoveBucket, isBypass, forceDelete bool, contentCh <-chan *mc.ClientContent) <-chan mc.RemoveResult {

View File

@@ -7124,7 +7124,7 @@ func init() {
"$ref": "#/definitions/putBucketRetentionRequest"
},
"versioning": {
"type": "boolean"
"$ref": "#/definitions/setBucketVersioning"
}
}
},
@@ -8195,8 +8195,18 @@ func init() {
"setBucketVersioning": {
"type": "object",
"properties": {
"versioning": {
"enabled": {
"type": "boolean"
},
"excludeFolders": {
"type": "boolean"
},
"excludePrefixes": {
"type": "array",
"maxLength": 10,
"items": {
"type": "string"
}
}
}
},
@@ -16257,7 +16267,7 @@ func init() {
"$ref": "#/definitions/putBucketRetentionRequest"
},
"versioning": {
"type": "boolean"
"$ref": "#/definitions/setBucketVersioning"
}
}
},
@@ -17328,8 +17338,18 @@ func init() {
"setBucketVersioning": {
"type": "object",
"properties": {
"versioning": {
"enabled": {
"type": "boolean"
},
"excludeFolders": {
"type": "boolean"
},
"excludePrefixes": {
"type": "array",
"maxLength": 10,
"items": {
"type": "string"
}
}
}
},

View File

@@ -188,8 +188,8 @@ const (
)
// removeBucket deletes a bucket
func doSetVersioning(ctx context.Context, client MCClient, state VersionState) error {
err := client.setVersioning(ctx, string(state))
func doSetVersioning(ctx context.Context, client MCClient, state VersionState, excludePrefix []string, excludeFolders bool) error {
err := client.setVersioning(ctx, string(state), excludePrefix, excludeFolders)
if err != nil {
return err.Cause
}
@@ -212,11 +212,19 @@ func setBucketVersioningResponse(session *models.Principal, params bucketApi.Set
versioningState := VersionSuspend
if params.Body.Versioning {
if params.Body.Enabled {
versioningState = VersionEnable
}
if err := doSetVersioning(ctx, amcClient, versioningState); err != nil {
var excludePrefixes []string
if params.Body.ExcludePrefixes != nil {
excludePrefixes = params.Body.ExcludePrefixes
}
excludeFolders := params.Body.ExcludeFolders
if err := doSetVersioning(ctx, amcClient, versioningState, excludePrefixes, excludeFolders); err != nil {
return ErrorWithContext(ctx, fmt.Errorf("error setting versioning for bucket: %s", err))
}
return nil
@@ -486,8 +494,14 @@ func getMakeBucketResponse(session *models.Principal, params bucketApi.MakeBucke
}
}()
versioningEnabled := false
if br.Versioning != nil && br.Versioning.Enabled {
versioningEnabled = true
}
// enable versioning if indicated or retention enabled
if br.Versioning || br.Retention != nil {
if versioningEnabled || br.Retention != nil {
s3Client, err := newS3BucketClient(session, *br.Name, "", getClientIP(params.HTTPRequest))
if err != nil {
return nil, ErrorWithContext(ctx, err)
@@ -496,7 +510,18 @@ func getMakeBucketResponse(session *models.Principal, params bucketApi.MakeBucke
// defining the client to be used
amcClient := mcClient{client: s3Client}
if err = doSetVersioning(ctx, amcClient, VersionEnable); err != nil {
excludePrefixes := []string{}
excludeFolders := false
if br.Versioning.ExcludeFolders && !br.Locking {
excludeFolders = true
}
if br.Versioning.ExcludePrefixes != nil && !br.Locking {
excludePrefixes = br.Versioning.ExcludePrefixes
}
if err = doSetVersioning(ctx, amcClient, VersionEnable, excludePrefixes, excludeFolders); err != nil {
return nil, ErrorWithContext(ctx, fmt.Errorf("error setting versioning for bucket: %s", err))
}
}

View File

@@ -50,7 +50,7 @@ var (
minioSetObjectLockConfigMock func(ctx context.Context, bucketName string, mode *minio.RetentionMode, validity *uint, unit *minio.ValidityUnit) error
minioGetBucketObjectLockConfigMock func(ctx context.Context, bucketName string) (mode *minio.RetentionMode, validity *uint, unit *minio.ValidityUnit, err error)
minioGetObjectLockConfigMock func(ctx context.Context, bucketName string) (lock string, mode *minio.RetentionMode, validity *uint, unit *minio.ValidityUnit, err error)
minioSetVersioningMock func(ctx context.Context, state string) *probe.Error
minioSetVersioningMock func(ctx context.Context, state string, excludePrefix []string, excludeFolders bool) *probe.Error
minioCopyObjectMock func(ctx context.Context, dst minio.CopyDestOptions, src minio.CopySrcOptions) (minio.UploadInfo, error)
minioSetBucketTaggingMock func(ctx context.Context, bucketName string, tags *tags.Tags) error
minioRemoveBucketTaggingMock func(ctx context.Context, bucketName string) error
@@ -112,8 +112,8 @@ func (mc minioClientMock) copyObject(ctx context.Context, dst minio.CopyDestOpti
return minioCopyObjectMock(ctx, dst, src)
}
func (c s3ClientMock) setVersioning(ctx context.Context, state string) *probe.Error {
return minioSetVersioningMock(ctx, state)
func (c s3ClientMock) setVersioning(ctx context.Context, state string, excludePrefix []string, excludeFolders bool) *probe.Error {
return minioSetVersioningMock(ctx, state, excludePrefix, excludeFolders)
}
func (mc minioClientMock) GetBucketTagging(ctx context.Context, bucketName string) (*tags.Tags, error) {
@@ -813,9 +813,11 @@ func Test_SetBucketVersioning(t *testing.T) {
type args struct {
ctx context.Context
state VersionState
excludePrefix []string
excludeFolders bool
bucketName string
client s3ClientMock
setVersioningFunc func(ctx context.Context, state string) *probe.Error
setVersioningFunc func(ctx context.Context, state string, excludePrefix []string, excludeFolders bool) *probe.Error
}
tests := []struct {
name string
@@ -829,7 +831,36 @@ func Test_SetBucketVersioning(t *testing.T) {
state: VersionEnable,
bucketName: "test",
client: minClient,
setVersioningFunc: func(ctx context.Context, state string) *probe.Error {
setVersioningFunc: func(ctx context.Context, state string, excludePrefix []string, excludeFolders bool) *probe.Error {
return nil
},
},
expectedError: nil,
},
{
name: "Set Bucket Version with Prefixes Success",
args: args{
ctx: ctx,
state: VersionEnable,
excludePrefix: []string{"prefix1", "prefix2"},
bucketName: "test",
client: minClient,
setVersioningFunc: func(ctx context.Context, state string, excludePrefix []string, excludeFolders bool) *probe.Error {
return nil
},
},
expectedError: nil,
},
{
name: "Set Bucket Version with Excluded Folders Success",
args: args{
ctx: ctx,
state: VersionEnable,
excludePrefix: []string{"prefix1", "prefix2"},
excludeFolders: true,
bucketName: "test",
client: minClient,
setVersioningFunc: func(ctx context.Context, state string, excludePrefix []string, excludeFolders bool) *probe.Error {
return nil
},
},
@@ -842,7 +873,7 @@ func Test_SetBucketVersioning(t *testing.T) {
state: VersionEnable,
bucketName: "test",
client: minClient,
setVersioningFunc: func(ctx context.Context, state string) *probe.Error {
setVersioningFunc: func(ctx context.Context, state string, excludePrefix []string, excludeFolders bool) *probe.Error {
return probe.NewError(errors.New(errorMsg))
},
},
@@ -854,7 +885,7 @@ func Test_SetBucketVersioning(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
minioSetVersioningMock = tt.args.setVersioningFunc
err := doSetVersioning(tt.args.ctx, tt.args.client, tt.args.state)
err := doSetVersioning(tt.args.ctx, tt.args.client, tt.args.state, tt.args.excludePrefix, tt.args.excludeFolders)
fmt.Println(t.Name())
fmt.Println("Expected:", tt.expectedError, "Error:", err)

View File

@@ -3756,7 +3756,7 @@ definitions:
locking:
type: boolean
versioning:
type: boolean
$ref: "#/definitions/setBucketVersioning"
quota:
$ref: "#/definitions/setBucketQuota"
retention:
@@ -4963,8 +4963,16 @@ definitions:
setBucketVersioning:
type: object
properties:
versioning:
enabled:
type: boolean
excludePrefixes:
type: array
maxLength: 10
items:
type: string
excludeFolders:
type: boolean
bucketObLockingResponse:
type: object
properties:
@@ -6154,4 +6162,4 @@ definitions:
groups:
type: array
items:
type: string
type: string