@scality/data-browser-library 1.1.5 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/__tests__/BucketReplicationFormPage.test.js +207 -2
- package/dist/components/__tests__/CreateFolderButton.test.js +67 -0
- package/dist/components/__tests__/DeleteObjectButton.test.js +249 -26
- package/dist/components/__tests__/ObjectList.test.js +65 -22
- package/dist/components/__tests__/UploadButton.test.js +80 -0
- package/dist/components/buckets/BucketReplicationFormPage.js +114 -22
- package/dist/components/objects/CreateFolderButton.js +2 -1
- package/dist/components/objects/DeleteObjectButton.js +89 -43
- package/dist/components/objects/ObjectList.js +21 -0
- package/dist/components/objects/UploadButton.js +2 -1
- package/dist/components/ui/DeleteObjectModalContent.d.ts +4 -1
- package/dist/components/ui/DeleteObjectModalContent.js +57 -18
- package/dist/hooks/__tests__/useDeleteFolder.test.d.ts +1 -0
- package/dist/hooks/__tests__/useDeleteFolder.test.js +203 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +2 -1
- package/dist/hooks/useDeleteFolder.d.ts +6 -0
- package/dist/hooks/useDeleteFolder.js +40 -0
- package/package.json +1 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { act } from "@testing-library/react";
|
|
2
|
+
import { renderHookWithWrapper } from "../../test/testUtils.js";
|
|
3
|
+
import { useS3Client } from "../useS3Client.js";
|
|
4
|
+
import { useDeleteFolder } from "../useDeleteFolder.js";
|
|
5
|
+
jest.mock('../useS3Client');
|
|
6
|
+
const mockSend = jest.fn();
|
|
7
|
+
const mockUseS3Client = jest.mocked(useS3Client);
|
|
8
|
+
describe('useDeleteFolder', ()=>{
|
|
9
|
+
beforeEach(()=>{
|
|
10
|
+
jest.clearAllMocks();
|
|
11
|
+
mockUseS3Client.mockReturnValue({
|
|
12
|
+
send: mockSend
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
it('deletes an empty folder with a single version', async ()=>{
|
|
16
|
+
mockSend.mockResolvedValueOnce({
|
|
17
|
+
Versions: [
|
|
18
|
+
{
|
|
19
|
+
Key: 'folder/',
|
|
20
|
+
VersionId: 'v1'
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
DeleteMarkers: [],
|
|
24
|
+
CommonPrefixes: [],
|
|
25
|
+
IsTruncated: false
|
|
26
|
+
});
|
|
27
|
+
mockSend.mockResolvedValueOnce({
|
|
28
|
+
Deleted: [
|
|
29
|
+
{
|
|
30
|
+
Key: 'folder/'
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
Errors: []
|
|
34
|
+
});
|
|
35
|
+
const { result } = renderHookWithWrapper(()=>useDeleteFolder());
|
|
36
|
+
await act(()=>result.current.mutateAsync({
|
|
37
|
+
Bucket: 'my-bucket',
|
|
38
|
+
FolderKey: 'folder/'
|
|
39
|
+
}));
|
|
40
|
+
expect(mockSend).toHaveBeenCalledTimes(2);
|
|
41
|
+
const deleteCommand = mockSend.mock.calls[1][0];
|
|
42
|
+
expect(deleteCommand.input).toEqual({
|
|
43
|
+
Bucket: 'my-bucket',
|
|
44
|
+
Delete: {
|
|
45
|
+
Objects: [
|
|
46
|
+
{
|
|
47
|
+
Key: 'folder/',
|
|
48
|
+
VersionId: 'v1'
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
it('deletes folder with multiple versions and delete markers', async ()=>{
|
|
55
|
+
mockSend.mockResolvedValueOnce({
|
|
56
|
+
Versions: [
|
|
57
|
+
{
|
|
58
|
+
Key: 'folder/',
|
|
59
|
+
VersionId: 'v1'
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
Key: 'folder/',
|
|
63
|
+
VersionId: 'v2'
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
DeleteMarkers: [
|
|
67
|
+
{
|
|
68
|
+
Key: 'folder/',
|
|
69
|
+
VersionId: 'dm1'
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
CommonPrefixes: [],
|
|
73
|
+
IsTruncated: false
|
|
74
|
+
});
|
|
75
|
+
mockSend.mockResolvedValueOnce({
|
|
76
|
+
Deleted: [],
|
|
77
|
+
Errors: []
|
|
78
|
+
});
|
|
79
|
+
const { result } = renderHookWithWrapper(()=>useDeleteFolder());
|
|
80
|
+
await act(()=>result.current.mutateAsync({
|
|
81
|
+
Bucket: 'my-bucket',
|
|
82
|
+
FolderKey: 'folder/'
|
|
83
|
+
}));
|
|
84
|
+
const deleteCommand = mockSend.mock.calls[1][0];
|
|
85
|
+
expect(deleteCommand.input.Delete.Objects).toEqual([
|
|
86
|
+
{
|
|
87
|
+
Key: 'folder/',
|
|
88
|
+
VersionId: 'v1'
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
Key: 'folder/',
|
|
92
|
+
VersionId: 'v2'
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
Key: 'folder/',
|
|
96
|
+
VersionId: 'dm1'
|
|
97
|
+
}
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
it('throws when folder has sub-folders (CommonPrefixes)', async ()=>{
|
|
101
|
+
mockSend.mockResolvedValueOnce({
|
|
102
|
+
Versions: [
|
|
103
|
+
{
|
|
104
|
+
Key: 'folder/',
|
|
105
|
+
VersionId: 'v1'
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
DeleteMarkers: [],
|
|
109
|
+
CommonPrefixes: [
|
|
110
|
+
{
|
|
111
|
+
Prefix: 'folder/sub/'
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
IsTruncated: false
|
|
115
|
+
});
|
|
116
|
+
const { result } = renderHookWithWrapper(()=>useDeleteFolder());
|
|
117
|
+
await expect(act(()=>result.current.mutateAsync({
|
|
118
|
+
Bucket: 'my-bucket',
|
|
119
|
+
FolderKey: 'folder/'
|
|
120
|
+
}))).rejects.toThrow('Cannot delete folder: The folder is not empty');
|
|
121
|
+
});
|
|
122
|
+
it('throws when folder contains other objects', async ()=>{
|
|
123
|
+
mockSend.mockResolvedValueOnce({
|
|
124
|
+
Versions: [
|
|
125
|
+
{
|
|
126
|
+
Key: 'folder/',
|
|
127
|
+
VersionId: 'v1'
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
Key: 'folder/file.txt',
|
|
131
|
+
VersionId: 'v2'
|
|
132
|
+
}
|
|
133
|
+
],
|
|
134
|
+
DeleteMarkers: [],
|
|
135
|
+
CommonPrefixes: [],
|
|
136
|
+
IsTruncated: false
|
|
137
|
+
});
|
|
138
|
+
const { result } = renderHookWithWrapper(()=>useDeleteFolder());
|
|
139
|
+
await expect(act(()=>result.current.mutateAsync({
|
|
140
|
+
Bucket: 'my-bucket',
|
|
141
|
+
FolderKey: 'folder/'
|
|
142
|
+
}))).rejects.toThrow('Cannot delete folder: The folder is not empty');
|
|
143
|
+
});
|
|
144
|
+
it('throws when ListObjectVersions response is truncated', async ()=>{
|
|
145
|
+
mockSend.mockResolvedValueOnce({
|
|
146
|
+
Versions: [
|
|
147
|
+
{
|
|
148
|
+
Key: 'folder/',
|
|
149
|
+
VersionId: 'v1'
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
DeleteMarkers: [],
|
|
153
|
+
CommonPrefixes: [],
|
|
154
|
+
IsTruncated: true
|
|
155
|
+
});
|
|
156
|
+
const { result } = renderHookWithWrapper(()=>useDeleteFolder());
|
|
157
|
+
await expect(act(()=>result.current.mutateAsync({
|
|
158
|
+
Bucket: 'my-bucket',
|
|
159
|
+
FolderKey: 'folder/'
|
|
160
|
+
}))).rejects.toThrow('Cannot delete folder: The folder is not empty');
|
|
161
|
+
});
|
|
162
|
+
it('throws when DeleteObjects returns Errors', async ()=>{
|
|
163
|
+
mockSend.mockResolvedValueOnce({
|
|
164
|
+
Versions: [
|
|
165
|
+
{
|
|
166
|
+
Key: 'folder/',
|
|
167
|
+
VersionId: 'v1'
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
DeleteMarkers: [],
|
|
171
|
+
CommonPrefixes: [],
|
|
172
|
+
IsTruncated: false
|
|
173
|
+
});
|
|
174
|
+
mockSend.mockResolvedValueOnce({
|
|
175
|
+
Errors: [
|
|
176
|
+
{
|
|
177
|
+
Key: 'folder/',
|
|
178
|
+
Code: 'AccessDenied',
|
|
179
|
+
Message: 'Access Denied because object protected by object lock.'
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
});
|
|
183
|
+
const { result } = renderHookWithWrapper(()=>useDeleteFolder());
|
|
184
|
+
await expect(act(()=>result.current.mutateAsync({
|
|
185
|
+
Bucket: 'my-bucket',
|
|
186
|
+
FolderKey: 'folder/'
|
|
187
|
+
}))).rejects.toThrow('Access Denied because object protected by object lock.');
|
|
188
|
+
});
|
|
189
|
+
it('does not call DeleteObjects when folder has no versions', async ()=>{
|
|
190
|
+
mockSend.mockResolvedValueOnce({
|
|
191
|
+
Versions: [],
|
|
192
|
+
DeleteMarkers: [],
|
|
193
|
+
CommonPrefixes: [],
|
|
194
|
+
IsTruncated: false
|
|
195
|
+
});
|
|
196
|
+
const { result } = renderHookWithWrapper(()=>useDeleteFolder());
|
|
197
|
+
await act(()=>result.current.mutateAsync({
|
|
198
|
+
Bucket: 'my-bucket',
|
|
199
|
+
FolderKey: 'folder/'
|
|
200
|
+
}));
|
|
201
|
+
expect(mockSend).toHaveBeenCalledTimes(1);
|
|
202
|
+
});
|
|
203
|
+
});
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export { useBatchObjectLegalHold } from './useBatchObjectLegalHold';
|
|
|
9
9
|
export { type BucketConfigEditorConfig, type BucketConfigEditorResult, useBucketConfigEditor } from './useBucketConfigEditor';
|
|
10
10
|
export { useBucketLocations } from './useBucketLocations';
|
|
11
11
|
export { useDeleteBucketConfigRule } from './useDeleteBucketConfigRule';
|
|
12
|
+
export { useDeleteFolder } from './useDeleteFolder';
|
|
12
13
|
export { useEmptyBucket } from './useEmptyBucket';
|
|
13
14
|
export { useFeatures } from './useFeatures';
|
|
14
15
|
export { useISVBucketStatus } from './useISVBucketDetection';
|
package/dist/hooks/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { useBatchObjectLegalHold } from "./useBatchObjectLegalHold.js";
|
|
|
8
8
|
import { useBucketConfigEditor } from "./useBucketConfigEditor.js";
|
|
9
9
|
import { useBucketLocations } from "./useBucketLocations.js";
|
|
10
10
|
import { useDeleteBucketConfigRule } from "./useDeleteBucketConfigRule.js";
|
|
11
|
+
import { useDeleteFolder } from "./useDeleteFolder.js";
|
|
11
12
|
import { useEmptyBucket } from "./useEmptyBucket.js";
|
|
12
13
|
import { useFeatures } from "./useFeatures.js";
|
|
13
14
|
import { useISVBucketStatus } from "./useISVBucketDetection.js";
|
|
@@ -17,4 +18,4 @@ import { useS3Client } from "./useS3Client.js";
|
|
|
17
18
|
import { useS3ConfigSwitch } from "./useS3ConfigSwitch.js";
|
|
18
19
|
import { useSupportedNotificationEvents } from "./useSupportedNotificationEvents.js";
|
|
19
20
|
import { useTableRowSelection } from "./useTableRowSelection.js";
|
|
20
|
-
export { getAccessibleBucketsStorageKey, getLimitedAccessFlagKey, setLimitedAccessFlag, useAccessibleBuckets, useBatchObjectLegalHold, useBucketConfigEditor, useBucketLocations, useBuckets, useCopyObject, useCreateBucket, useCreateFolder, useDeleteBucket, useDeleteBucketConfigRule, useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useDeleteObject, useDeleteObjectTagging, useDeleteObjects, useDeletePublicAccessBlock, useEmptyBucket, useFeatures, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketLocation, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetObject, useGetObjectAttributes, useGetObjectTorrent, useGetPresignedDownload, useGetPresignedPost, useGetPresignedUpload, useGetPublicAccessBlock, useISVBucketStatus, useIsBucketEmpty, useLimitedAccessFlow, useListMultipartUploads, useListObjectVersions, useListObjects, useLoginMutation, useObjectAcl, useObjectLegalHold, useObjectMetadata, useObjectRetention, useObjectTagging, usePutObject, usePutPublicAccessBlock, useRestoreObject, useS3Client, useS3ConfigSwitch, useSearchObjects, useSearchObjectsVersions, useSelectObjectContent, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning, useSetObjectAcl, useSetObjectLegalHold, useSetObjectRetention, useSetObjectTagging, useSupportedNotificationEvents, useTableRowSelection, useUploadObjects, useValidateBucketAccess };
|
|
21
|
+
export { getAccessibleBucketsStorageKey, getLimitedAccessFlagKey, setLimitedAccessFlag, useAccessibleBuckets, useBatchObjectLegalHold, useBucketConfigEditor, useBucketLocations, useBuckets, useCopyObject, useCreateBucket, useCreateFolder, useDeleteBucket, useDeleteBucketConfigRule, useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useDeleteFolder, useDeleteObject, useDeleteObjectTagging, useDeleteObjects, useDeletePublicAccessBlock, useEmptyBucket, useFeatures, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketLocation, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetObject, useGetObjectAttributes, useGetObjectTorrent, useGetPresignedDownload, useGetPresignedPost, useGetPresignedUpload, useGetPublicAccessBlock, useISVBucketStatus, useIsBucketEmpty, useLimitedAccessFlow, useListMultipartUploads, useListObjectVersions, useListObjects, useLoginMutation, useObjectAcl, useObjectLegalHold, useObjectMetadata, useObjectRetention, useObjectTagging, usePutObject, usePutPublicAccessBlock, useRestoreObject, useS3Client, useS3ConfigSwitch, useSearchObjects, useSearchObjectsVersions, useSelectObjectContent, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning, useSetObjectAcl, useSetObjectLegalHold, useSetObjectRetention, useSetObjectTagging, useSupportedNotificationEvents, useTableRowSelection, useUploadObjects, useValidateBucketAccess };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
interface DeleteFolderInput {
|
|
2
|
+
Bucket: string;
|
|
3
|
+
FolderKey: string;
|
|
4
|
+
}
|
|
5
|
+
export declare const useDeleteFolder: (options?: Omit<import("@tanstack/react-query").UseMutationOptions<void, import("..").EnhancedS3Error, DeleteFolderInput, unknown>, "mutationFn"> | undefined) => import("@tanstack/react-query").UseMutationResult<void, import("..").EnhancedS3Error, DeleteFolderInput>;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { DeleteObjectsCommand, ListObjectVersionsCommand } from "@aws-sdk/client-s3";
|
|
2
|
+
import { useCreateS3FunctionMutationHook } from "./factories/useCreateS3MutationHook.js";
|
|
3
|
+
async function deleteFolder(s3Client, input) {
|
|
4
|
+
const { Bucket, FolderKey } = input;
|
|
5
|
+
const listResponse = await s3Client.send(new ListObjectVersionsCommand({
|
|
6
|
+
Bucket,
|
|
7
|
+
Prefix: FolderKey,
|
|
8
|
+
Delimiter: '/'
|
|
9
|
+
}));
|
|
10
|
+
const versions = listResponse.Versions ?? [];
|
|
11
|
+
const deleteMarkers = listResponse.DeleteMarkers ?? [];
|
|
12
|
+
const commonPrefixes = listResponse.CommonPrefixes ?? [];
|
|
13
|
+
const filteredVersions = versions.filter((v)=>v.Key === FolderKey);
|
|
14
|
+
const filteredDeleteMarkers = deleteMarkers.filter((v)=>v.Key === FolderKey);
|
|
15
|
+
if (listResponse.IsTruncated || commonPrefixes.length > 0 || filteredVersions.length + filteredDeleteMarkers.length < versions.length + deleteMarkers.length) throw new Error('Cannot delete folder: The folder is not empty');
|
|
16
|
+
const objects = [
|
|
17
|
+
...filteredVersions.map((v)=>({
|
|
18
|
+
Key: v.Key,
|
|
19
|
+
VersionId: v.VersionId
|
|
20
|
+
})),
|
|
21
|
+
...filteredDeleteMarkers.map((v)=>({
|
|
22
|
+
Key: v.Key,
|
|
23
|
+
VersionId: v.VersionId
|
|
24
|
+
}))
|
|
25
|
+
];
|
|
26
|
+
if (objects.length > 0) {
|
|
27
|
+
const deleteResponse = await s3Client.send(new DeleteObjectsCommand({
|
|
28
|
+
Bucket,
|
|
29
|
+
Delete: {
|
|
30
|
+
Objects: objects
|
|
31
|
+
}
|
|
32
|
+
}));
|
|
33
|
+
if (deleteResponse.Errors && deleteResponse.Errors.length > 0) {
|
|
34
|
+
const firstError = deleteResponse.Errors[0];
|
|
35
|
+
throw new Error(firstError.Message ?? 'Failed to delete folder');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const useDeleteFolder = useCreateS3FunctionMutationHook(deleteFolder);
|
|
40
|
+
export { useDeleteFolder };
|