@aws-amplify/ui-react-storage 3.14.0 → 3.15.0

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.
Files changed (66) hide show
  1. package/dist/browser.js +8 -2
  2. package/dist/{createStorageBrowser-CotOvK0A.js → createStorageBrowser-CG-6mXiT.js} +392 -148
  3. package/dist/esm/browser.mjs +1 -0
  4. package/dist/esm/components/StorageBrowser/ErrorBoundary/ErrorBoundary.mjs +1 -0
  5. package/dist/esm/components/StorageBrowser/StorageBrowserAmplify.mjs +1 -0
  6. package/dist/esm/components/StorageBrowser/actions/configs/defaults.mjs +1 -0
  7. package/dist/esm/components/StorageBrowser/actions/handlers/defaults.mjs +1 -1
  8. package/dist/esm/components/StorageBrowser/actions/handlers/listLocations.mjs +7 -2
  9. package/dist/esm/components/StorageBrowser/actions/handlers/utils.mjs +65 -1
  10. package/dist/esm/components/StorageBrowser/actions/handlers/zipdownload.mjs +195 -0
  11. package/dist/esm/components/StorageBrowser/adapters/createAmplifyAuthAdapter/createAmplifyListLocationsHandler.mjs +3 -1
  12. package/dist/esm/components/StorageBrowser/adapters/createManagedAuthAdapter/createManagedAuthAdapter.mjs +1 -0
  13. package/dist/esm/components/StorageBrowser/components/ComponentsProvider.mjs +1 -0
  14. package/dist/esm/components/StorageBrowser/components/base/preview/DownloadButton.mjs +1 -0
  15. package/dist/esm/components/StorageBrowser/controls/DataTableControl.mjs +1 -0
  16. package/dist/esm/components/StorageBrowser/createStorageBrowser/StorageBrowserDefault.mjs +1 -0
  17. package/dist/esm/components/StorageBrowser/createStorageBrowser/createProvider.mjs +1 -0
  18. package/dist/esm/components/StorageBrowser/createStorageBrowser/createStorageBrowser.mjs +1 -0
  19. package/dist/esm/components/StorageBrowser/displayText/libraries/en/downloadView.mjs +2 -0
  20. package/dist/esm/components/StorageBrowser/displayText/libraries/en/shared.mjs +2 -0
  21. package/dist/esm/components/StorageBrowser/locationItems/context.mjs +1 -0
  22. package/dist/esm/components/StorageBrowser/tasks/constants.mjs +2 -0
  23. package/dist/esm/components/StorageBrowser/tasks/useProcessTasks.mjs +10 -4
  24. package/dist/esm/components/StorageBrowser/tasks/utils.mjs +4 -1
  25. package/dist/esm/components/StorageBrowser/useAction/useHandler.mjs +1 -1
  26. package/dist/esm/components/StorageBrowser/useAction/utils.mjs +1 -0
  27. package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/CopyView.mjs +1 -0
  28. package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/CopyViewProvider.mjs +1 -0
  29. package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/FoldersMessageControl.mjs +1 -0
  30. package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/useCopyView.mjs +1 -0
  31. package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/useFolders.mjs +1 -0
  32. package/dist/esm/components/StorageBrowser/views/LocationActionView/CreateFolderView/CreateFolderView.mjs +1 -0
  33. package/dist/esm/components/StorageBrowser/views/LocationActionView/CreateFolderView/useCreateFolderView.mjs +1 -0
  34. package/dist/esm/components/StorageBrowser/views/LocationActionView/DeleteView/DeleteView.mjs +1 -0
  35. package/dist/esm/components/StorageBrowser/views/LocationActionView/DeleteView/useDeleteView.mjs +1 -0
  36. package/dist/esm/components/StorageBrowser/views/LocationActionView/DownloadView/DownloadView.mjs +1 -0
  37. package/dist/esm/components/StorageBrowser/views/LocationActionView/DownloadView/useDownloadView.mjs +1 -0
  38. package/dist/esm/components/StorageBrowser/views/LocationActionView/UploadView/UploadView.mjs +1 -0
  39. package/dist/esm/components/StorageBrowser/views/LocationActionView/UploadView/UploadViewProvider.mjs +1 -0
  40. package/dist/esm/components/StorageBrowser/views/LocationActionView/UploadView/useUploadView.mjs +1 -0
  41. package/dist/esm/components/StorageBrowser/views/LocationDetailView/LocationDetailView.mjs +1 -0
  42. package/dist/esm/components/StorageBrowser/views/LocationDetailView/getLocationDetailViewTableData/getLocationDetailViewTableData.mjs +1 -0
  43. package/dist/esm/components/StorageBrowser/views/LocationDetailView/useLocationDetailView.mjs +1 -0
  44. package/dist/esm/components/StorageBrowser/views/LocationsView/LocationsView.mjs +1 -0
  45. package/dist/esm/components/StorageBrowser/views/LocationsView/LocationsViewProvider.mjs +1 -0
  46. package/dist/esm/components/StorageBrowser/views/LocationsView/useLocationsView.mjs +1 -0
  47. package/dist/esm/components/StorageBrowser/views/context/actionViews.mjs +1 -0
  48. package/dist/esm/components/StorageBrowser/views/context/primaryViews.mjs +1 -0
  49. package/dist/esm/components/StorageBrowser/views/hooks/useFilePreview/useFilePreview.mjs +1 -0
  50. package/dist/esm/components/StorageBrowser/views/utils/tableResolvers/constants.mjs +4 -0
  51. package/dist/esm/components/StorageBrowser/views/utils/tableResolvers/utils.mjs +1 -1
  52. package/dist/esm/version.mjs +1 -1
  53. package/dist/index.js +2 -1
  54. package/dist/styles.css +17 -0
  55. package/dist/types/components/StorageBrowser/actions/handlers/index.d.ts +1 -0
  56. package/dist/types/components/StorageBrowser/actions/handlers/types.d.ts +4 -2
  57. package/dist/types/components/StorageBrowser/actions/handlers/utils.d.ts +11 -0
  58. package/dist/types/components/StorageBrowser/actions/handlers/zipdownload.d.ts +3 -0
  59. package/dist/types/components/StorageBrowser/displayText/types.d.ts +2 -0
  60. package/dist/types/components/StorageBrowser/tasks/types.d.ts +3 -3
  61. package/dist/types/components/StorageBrowser/tasks/useProcessTasks.d.ts +1 -1
  62. package/dist/types/components/StorageBrowser/useAction/useHandler.d.ts +1 -1
  63. package/dist/types/components/StorageBrowser/views/utils/tableResolvers/constants.d.ts +4 -0
  64. package/dist/types/version.d.ts +1 -1
  65. package/package.json +11 -8
  66. package/dist/esm/components/StorageBrowser/actions/handlers/download.mjs +0 -38
@@ -10,8 +10,11 @@ var uiReact = require('@aws-amplify/ui-react');
10
10
  var elements = require('@aws-amplify/ui-react-core/elements');
11
11
  var internal = require('@aws-amplify/ui-react/internal');
12
12
  var uiReactCore = require('@aws-amplify/ui-react-core');
13
+ var JSZip = require('jszip');
13
14
  var storage = require('aws-amplify/storage');
14
15
 
16
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
17
+
15
18
  function _interopNamespace(e) {
16
19
  if (e && e.__esModule) return e;
17
20
  var n = Object.create(null);
@@ -31,8 +34,185 @@ function _interopNamespace(e) {
31
34
  }
32
35
 
33
36
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
37
+ var JSZip__default = /*#__PURE__*/_interopDefault(JSZip);
38
+
39
+ const VERSION = '3.15.0';
40
+
41
+ const DEFAULT_CHECKSUM_ALGORITHM = 'crc-32';
42
+ // 5MiB for multipart upload
43
+ // https://github.com/aws-amplify/amplify-js/blob/1a5366d113c9af4ce994168653df3aadb142c581/packages/storage/src/providers/s3/utils/constants.ts#L16
44
+ const MULTIPART_UPLOAD_THRESHOLD_BYTES = 5 * 1024 * 1024;
34
45
 
35
- const VERSION = '3.14.0';
46
+ const getBucketRegion = (bucketName, fallbackRegion) => {
47
+ try {
48
+ const config = awsAmplify.Amplify.getConfig()?.Storage?.S3;
49
+ if (!config?.buckets || typeof config.buckets !== 'object') {
50
+ return fallbackRegion;
51
+ }
52
+ for (const bucketConfig of Object.values(config.buckets)) {
53
+ if (bucketConfig.bucketName === bucketName && bucketConfig.region) {
54
+ return bucketConfig.region;
55
+ }
56
+ }
57
+ return fallbackRegion;
58
+ }
59
+ catch (error) {
60
+ return fallbackRegion;
61
+ }
62
+ };
63
+ const constructBucket$1 = ({ bucket: bucketName, region: globalRegion, }) => {
64
+ const bucketRegion = getBucketRegion(bucketName, globalRegion);
65
+ return { bucketName, region: bucketRegion };
66
+ };
67
+ const parseAccessGrantLocation = (location) => {
68
+ const { permission, scope, type } = location;
69
+ if (!scope.startsWith('s3://')) {
70
+ throw new Error(`Invalid scope: ${scope}`);
71
+ }
72
+ const id = crypto.randomUUID();
73
+ // remove default path
74
+ const slicedScope = scope.slice(5);
75
+ let bucket, prefix;
76
+ switch (type) {
77
+ case 'BUCKET': {
78
+ // { scope: 's3://bucket/*', type: 'BUCKET', },
79
+ bucket = slicedScope.slice(0, -2);
80
+ prefix = '';
81
+ break;
82
+ }
83
+ case 'PREFIX': {
84
+ // { scope: 's3://bucket/path/*', type: 'PREFIX', },
85
+ bucket = slicedScope.slice(0, slicedScope.indexOf('/'));
86
+ prefix = `${slicedScope.slice(bucket.length + 1, -1)}`;
87
+ break;
88
+ }
89
+ case 'OBJECT': {
90
+ // { scope: 's3://bucket/path/to/object', type: 'OBJECT', },
91
+ bucket = slicedScope.slice(0, slicedScope.indexOf('/'));
92
+ prefix = slicedScope.slice(bucket.length + 1);
93
+ break;
94
+ }
95
+ default: {
96
+ throw new Error(`Invalid location type: ${type}`);
97
+ }
98
+ }
99
+ let permissions;
100
+ switch (permission) {
101
+ case 'READ':
102
+ permissions = ['get', 'list'];
103
+ break;
104
+ case 'READWRITE':
105
+ permissions = ['delete', 'get', 'list', 'write'];
106
+ break;
107
+ case 'WRITE':
108
+ permissions = ['delete', 'write'];
109
+ break;
110
+ default:
111
+ throw new Error(`Invalid location permission: ${permission}`);
112
+ }
113
+ return { bucket, id, permissions: permissions, prefix, type };
114
+ };
115
+ const isSamePermissions = (permissionsToExclude, locationPermissions) => {
116
+ if (permissionsToExclude.length !== locationPermissions.length) {
117
+ return false;
118
+ }
119
+ const sortedLocationPermissions = locationPermissions.sort();
120
+ return permissionsToExclude
121
+ .sort()
122
+ .every((permission, index) => permission === sortedLocationPermissions[index]);
123
+ };
124
+ const isSameType = (typeToExclude, locationType) => typeof typeToExclude === 'string'
125
+ ? typeToExclude === locationType
126
+ : typeToExclude.includes(locationType);
127
+ const shouldExcludeLocation = ({ permissions, type }, exclude) => {
128
+ const excludedByPermssions = !!(exclude?.exactPermissions &&
129
+ isSamePermissions(exclude.exactPermissions, permissions));
130
+ const excludedByType = !!(exclude?.type && isSameType(exclude.type, type));
131
+ return excludedByPermssions || excludedByType;
132
+ };
133
+ /**
134
+ * Determines if permissions1 is a strict superset (broader) of permissions2.
135
+ * Returns true only if permissions1 contains all permissions from permissions2
136
+ * AND has more permissions.
137
+ */
138
+ const hasBroaderPermissions = (permissions1, permissions2) => {
139
+ // permissions1 must have more permissions (strict superset, not equal)
140
+ if (permissions1.length <= permissions2.length) {
141
+ return false;
142
+ }
143
+ // permissions1 must contain all permissions from permissions2
144
+ return permissions2.every((perm) => permissions1.includes(perm));
145
+ };
146
+ /**
147
+ * Deduplicates locations with the same bucket and prefix.
148
+ * Only deduplicates when one location's permissions are a superset of another's.
149
+ * This prevents deduplication of incompatible grants like READ + WRITE.
150
+ *
151
+ * Examples:
152
+ * - READ + READWRITE → Keep READWRITE (superset)
153
+ * - READ + READ → Keep first (identical)
154
+ * - READ + WRITE → Keep both (not superset, need separate locations)
155
+ */
156
+ const deduplicateLocations = (locations) => {
157
+ // Group locations by bucket:prefix
158
+ const locationGroups = new Map();
159
+ for (const location of locations) {
160
+ const key = `${location.bucket}:${location.prefix}`;
161
+ const group = locationGroups.get(key) ?? [];
162
+ group.push(location);
163
+ locationGroups.set(key, group);
164
+ }
165
+ // For each group, keep only non-redundant locations
166
+ const result = [];
167
+ for (const group of locationGroups.values()) {
168
+ if (group.length === 1) {
169
+ result.push(group[0]);
170
+ continue;
171
+ }
172
+ // Find locations that are not subsets of any other location in the group
173
+ const nonRedundant = [];
174
+ for (const location of group) {
175
+ // Check if this location is a subset of any other location
176
+ const isSubsetOfAnother = group.some((other) => {
177
+ if (other === location)
178
+ return false;
179
+ return hasBroaderPermissions(other.permissions, location.permissions);
180
+ });
181
+ if (!isSubsetOfAnother) {
182
+ // Check if we already have an identical permission set
183
+ const isDuplicate = nonRedundant.some((existing) => {
184
+ const sortedNew = [...location.permissions].sort().join(',');
185
+ const sortedExisting = [...existing.permissions].sort().join(',');
186
+ return sortedNew === sortedExisting;
187
+ });
188
+ if (!isDuplicate) {
189
+ nonRedundant.push(location);
190
+ }
191
+ }
192
+ }
193
+ result.push(...nonRedundant);
194
+ }
195
+ return result;
196
+ };
197
+ const getFilteredLocations = (locations, exclude) => locations.reduce((filteredLocations, location) => {
198
+ const parsedLocation = parseAccessGrantLocation(location);
199
+ const isNonFolderLikePrefix = !parsedLocation.prefix.endsWith('/') &&
200
+ parsedLocation.type === 'PREFIX';
201
+ if (isNonFolderLikePrefix) {
202
+ return filteredLocations;
203
+ }
204
+ if (!shouldExcludeLocation(parsedLocation, exclude)) {
205
+ filteredLocations.push(parsedLocation);
206
+ }
207
+ return filteredLocations;
208
+ }, []);
209
+ const getFileKey = (key) => key.slice(key.lastIndexOf('/') + 1, key.length);
210
+ const createFileDataItem = (data) => ({
211
+ ...data,
212
+ fileKey: getFileKey(data.key),
213
+ });
214
+ const getProgress = ({ totalBytes, transferredBytes, }) => totalBytes ? transferredBytes / totalBytes : undefined;
215
+ const isMultipartUpload = (file) => file.size > MULTIPART_UPLOAD_THRESHOLD_BYTES;
36
216
 
37
217
  const toAccessGrantPermission = (permission) => {
38
218
  let result = '';
@@ -118,7 +298,8 @@ const createAmplifyListLocationsHandler = () => {
118
298
  id: crypto.randomUUID(),
119
299
  };
120
300
  });
121
- cachedItems = sanitizedItems;
301
+ // Deduplicate locations with the same bucket and prefix, keeping broader permissions
302
+ cachedItems = deduplicateLocations(sanitizedItems);
122
303
  return getPaginatedLocations({
123
304
  items: cachedItems,
124
305
  pageSize,
@@ -163,123 +344,11 @@ const createAmplifyAuthAdapter = () => {
163
344
  };
164
345
  };
165
346
 
166
- const DEFAULT_CHECKSUM_ALGORITHM = 'crc-32';
167
- // 5MiB for multipart upload
168
- // https://github.com/aws-amplify/amplify-js/blob/1a5366d113c9af4ce994168653df3aadb142c581/packages/storage/src/providers/s3/utils/constants.ts#L16
169
- const MULTIPART_UPLOAD_THRESHOLD_BYTES = 5 * 1024 * 1024;
170
-
171
- const getBucketRegion = (bucketName, fallbackRegion) => {
172
- try {
173
- const config = awsAmplify.Amplify.getConfig()?.Storage?.S3;
174
- if (!config?.buckets || typeof config.buckets !== 'object') {
175
- return fallbackRegion;
176
- }
177
- for (const bucketConfig of Object.values(config.buckets)) {
178
- if (bucketConfig.bucketName === bucketName && bucketConfig.region) {
179
- return bucketConfig.region;
180
- }
181
- }
182
- return fallbackRegion;
183
- }
184
- catch (error) {
185
- return fallbackRegion;
186
- }
187
- };
188
- const constructBucket = ({ bucket: bucketName, region: globalRegion, }) => {
189
- const bucketRegion = getBucketRegion(bucketName, globalRegion);
190
- return { bucketName, region: bucketRegion };
191
- };
192
- const parseAccessGrantLocation = (location) => {
193
- const { permission, scope, type } = location;
194
- if (!scope.startsWith('s3://')) {
195
- throw new Error(`Invalid scope: ${scope}`);
196
- }
197
- const id = crypto.randomUUID();
198
- // remove default path
199
- const slicedScope = scope.slice(5);
200
- let bucket, prefix;
201
- switch (type) {
202
- case 'BUCKET': {
203
- // { scope: 's3://bucket/*', type: 'BUCKET', },
204
- bucket = slicedScope.slice(0, -2);
205
- prefix = '';
206
- break;
207
- }
208
- case 'PREFIX': {
209
- // { scope: 's3://bucket/path/*', type: 'PREFIX', },
210
- bucket = slicedScope.slice(0, slicedScope.indexOf('/'));
211
- prefix = `${slicedScope.slice(bucket.length + 1, -1)}`;
212
- break;
213
- }
214
- case 'OBJECT': {
215
- // { scope: 's3://bucket/path/to/object', type: 'OBJECT', },
216
- bucket = slicedScope.slice(0, slicedScope.indexOf('/'));
217
- prefix = slicedScope.slice(bucket.length + 1);
218
- break;
219
- }
220
- default: {
221
- throw new Error(`Invalid location type: ${type}`);
222
- }
223
- }
224
- let permissions;
225
- switch (permission) {
226
- case 'READ':
227
- permissions = ['get', 'list'];
228
- break;
229
- case 'READWRITE':
230
- permissions = ['delete', 'get', 'list', 'write'];
231
- break;
232
- case 'WRITE':
233
- permissions = ['delete', 'write'];
234
- break;
235
- default:
236
- throw new Error(`Invalid location permission: ${permission}`);
237
- }
238
- return { bucket, id, permissions: permissions, prefix, type };
239
- };
240
- const isSamePermissions = (permissionsToExclude, locationPermissions) => {
241
- if (permissionsToExclude.length !== locationPermissions.length) {
242
- return false;
243
- }
244
- const sortedLocationPermissions = locationPermissions.sort();
245
- return permissionsToExclude
246
- .sort()
247
- .every((permission, index) => permission === sortedLocationPermissions[index]);
248
- };
249
- const isSameType = (typeToExclude, locationType) => typeof typeToExclude === 'string'
250
- ? typeToExclude === locationType
251
- : typeToExclude.includes(locationType);
252
- const shouldExcludeLocation = ({ permissions, type }, exclude) => {
253
- const excludedByPermssions = !!(exclude?.exactPermissions &&
254
- isSamePermissions(exclude.exactPermissions, permissions));
255
- const excludedByType = !!(exclude?.type && isSameType(exclude.type, type));
256
- return excludedByPermssions || excludedByType;
257
- };
258
- const getFilteredLocations = (locations, exclude) => locations.reduce((filteredLocations, location) => {
259
- const parsedLocation = parseAccessGrantLocation(location);
260
- const isNonFolderLikePrefix = !parsedLocation.prefix.endsWith('/') &&
261
- parsedLocation.type === 'PREFIX';
262
- if (isNonFolderLikePrefix) {
263
- return filteredLocations;
264
- }
265
- if (!shouldExcludeLocation(parsedLocation, exclude)) {
266
- filteredLocations.push(parsedLocation);
267
- }
268
- return filteredLocations;
269
- }, []);
270
- const getFileKey = (key) => key.slice(key.lastIndexOf('/') + 1, key.length);
271
- const createFileDataItem = (data) => ({
272
- ...data,
273
- fileKey: getFileKey(data.key),
274
- });
275
- const getProgress = ({ totalBytes, transferredBytes, }) => totalBytes ? transferredBytes / totalBytes : undefined;
276
- const isMultipartUpload = (file) => file.size > MULTIPART_UPLOAD_THRESHOLD_BYTES;
277
-
278
347
  const copyHandler = (input) => {
279
348
  const { config, data } = input;
280
349
  const { accountId: expectedBucketOwner, credentials, customEndpoint, } = config;
281
350
  const { key, sourceKey, lastModified, eTag } = data;
282
- const bucket = constructBucket(config);
351
+ const bucket = constructBucket$1(config);
283
352
  const source = {
284
353
  bucket,
285
354
  expectedBucketOwner,
@@ -320,7 +389,7 @@ const createFolderHandler = (input) => {
320
389
  const { accountId, credentials, customEndpoint } = config;
321
390
  const { onProgress } = options ?? {};
322
391
  const { key, preventOverwrite } = data;
323
- const bucket = constructBucket(config);
392
+ const bucket = constructBucket$1(config);
324
393
  const { result } = internals.uploadData({
325
394
  path: key,
326
395
  data: '',
@@ -359,7 +428,7 @@ const deleteHandler = ({ config, data, }) => {
359
428
  const result = internals.remove({
360
429
  path: key,
361
430
  options: {
362
- bucket: constructBucket(config),
431
+ bucket: constructBucket$1(config),
363
432
  locationCredentialsProvider: credentials,
364
433
  expectedBucketOwner: accountId,
365
434
  customEndpoint,
@@ -376,19 +445,115 @@ const deleteHandler = ({ config, data, }) => {
376
445
  return { result };
377
446
  };
378
447
 
379
- function downloadFromUrl(fileName, url) {
380
- const a = document.createElement('a');
381
- a.href = url;
382
- a.download = fileName;
383
- a.target = '_blank';
384
- document.body.appendChild(a);
385
- a.click();
386
- document.body.removeChild(a);
387
- }
388
- const downloadHandler = ({ config, data }) => {
389
- const { accountId, credentials, customEndpoint } = config;
448
+ const zipProgressManager = ({ dataMap, onZipProgress, }) => {
449
+ const iter = (() => {
450
+ let f;
451
+ let i = 0;
452
+ return (str) => {
453
+ if (!f) {
454
+ f = str;
455
+ }
456
+ else if (str !== f) {
457
+ ++i;
458
+ f = str;
459
+ }
460
+ return i;
461
+ };
462
+ })();
463
+ const progressMap = new Map(Array.from(dataMap.keys()).map((k) => [k, 0]));
464
+ const total = dataMap.size;
465
+ dataMap.forEach((data, key) => {
466
+ onZipProgress(key, 0, data);
467
+ });
468
+ return ({ percent, currentFile, }) => {
469
+ if (currentFile) {
470
+ const item = iter(currentFile);
471
+ const sliceSize = 100 / total; // when 3 this is 33.3%
472
+ const start = sliceSize * item; // this is 66.6% for last item of 3;
473
+ // take percent and calculate the percent of the slice
474
+ const progress = percent - start;
475
+ const actualPercent = (progress / sliceSize) * 100;
476
+ progressMap.set(currentFile, Math.max(actualPercent, progressMap.get(currentFile) ?? 0));
477
+ onZipProgress(currentFile, progressMap.get(currentFile), dataMap.get(currentFile));
478
+ }
479
+ };
480
+ };
481
+ const zipper = (() => {
482
+ let zip = null;
483
+ const dataMap = new Map();
484
+ return {
485
+ addFile: (file, name, data) => {
486
+ if (!zip) {
487
+ zip = new JSZip__default["default"]();
488
+ }
489
+ dataMap.set(name, data);
490
+ return new Promise((ok, no) => {
491
+ try {
492
+ zip?.file(name, file);
493
+ ok();
494
+ }
495
+ catch (e) {
496
+ no();
497
+ }
498
+ });
499
+ },
500
+ getBlobUrl: async (onProgress) => {
501
+ if (!zip) {
502
+ throw new Error('no zip');
503
+ }
504
+ const blob = await zip.generateAsync({
505
+ type: 'blob',
506
+ streamFiles: true,
507
+ compression: 'DEFLATE',
508
+ compressionOptions: {
509
+ level: 5,
510
+ },
511
+ }, zipProgressManager({
512
+ dataMap,
513
+ onZipProgress: (file, progress, data) => {
514
+ if (ui.isFunction(onProgress)) {
515
+ onProgress(progress, file, data);
516
+ }
517
+ },
518
+ }));
519
+ zip = null;
520
+ return URL.createObjectURL(blob);
521
+ },
522
+ destroy: () => {
523
+ dataMap.clear();
524
+ zip = null;
525
+ },
526
+ };
527
+ })();
528
+ const constructBucket = ({ bucket: bucketName, region, }) => ({ bucketName, region });
529
+ const readBody = async (response, { data, options }) => {
530
+ let loading = true;
531
+ const chunks = [];
532
+ const reader = response.body.getReader();
533
+ const size = +(response.headers.get('content-length') ?? 0);
534
+ let received = 0;
535
+ while (loading) {
536
+ const { value, done } = await reader.read();
537
+ if (done) {
538
+ loading = false;
539
+ }
540
+ else {
541
+ chunks.push(value);
542
+ received += value.length;
543
+ if (ui.isFunction(options?.onProgress)) {
544
+ options?.onProgress(data, getProgress({
545
+ totalBytes: size,
546
+ transferredBytes: received,
547
+ }), 'PENDING');
548
+ }
549
+ }
550
+ }
551
+ return new Blob(chunks);
552
+ };
553
+ const download = async ({ config, data, all, options }, abortController) => {
554
+ const { customEndpoint, credentials, accountId } = config;
390
555
  const { key } = data;
391
- const result = internals.getUrl({
556
+ const { url } = await internals.getUrl({
392
557
  path: key,
393
558
  options: {
394
559
  bucket: constructBucket(config),
@@ -398,17 +563,76 @@ const downloadHandler = ({ config, data }) => {
398
563
  contentDisposition: 'attachment',
399
564
  expectedBucketOwner: accountId,
400
565
  },
401
- })
402
- .then(({ url }) => {
403
- downloadFromUrl(key, url.toString());
404
- return { status: 'COMPLETE', value: { url } };
405
- })
406
- .catch((error) => {
407
- const { message } = error;
408
- return { error, message, status: 'FAILED' };
409
566
  });
410
- return { result };
411
- };
567
+ const response = await fetch(url, {
568
+ mode: 'cors',
569
+ signal: abortController.signal,
570
+ });
571
+ const blob = await readBody(response, { data, options });
572
+ const [filename] = key.split('/').reverse();
573
+ await zipper.addFile(blob, filename, data);
574
+ return filename;
575
+ };
576
+ const downloadHandler = (() => {
577
+ const fileDownloadQueue = new Map();
578
+ const handler = ({ config, data, all, options }) => {
579
+ const { key } = data;
580
+ const [, folder] = key.split('/').reverse();
581
+ fileDownloadQueue.set(key, false);
582
+ const abortController = new AbortController();
583
+ return {
584
+ cancel: () => {
585
+ abortController.abort();
586
+ fileDownloadQueue.set(key, true);
587
+ },
588
+ result: download({ config, data, all, options }, abortController)
589
+ .then(() => {
590
+ fileDownloadQueue.set(key, true);
591
+ return {
592
+ status: 'LOADED',
593
+ };
594
+ })
595
+ .catch((e) => {
596
+ const error = e;
597
+ fileDownloadQueue.set(key, true);
598
+ return {
599
+ status: 'FAILED',
600
+ message: error.message,
601
+ error,
602
+ };
603
+ })
604
+ .finally(() => {
605
+ const done = all.every(({ key }) => {
606
+ return fileDownloadQueue.get(key);
607
+ });
608
+ if (done) {
609
+ zipper
610
+ .getBlobUrl((percent, name, _data) => {
611
+ if (ui.isFunction(options?.onProgress)) {
612
+ const progress = percent / 100;
613
+ options?.onProgress(_data, progress, progress === 1 ? 'COMPLETE' : 'FINISHING');
614
+ }
615
+ })
616
+ .then((blobURL) => {
617
+ if (blobURL) {
618
+ zipper.destroy();
619
+ const anchor = document.createElement('a');
620
+ const clickEvent = new MouseEvent('click');
621
+ anchor.href = blobURL;
622
+ anchor.download = `${folder || 'archive'}.zip`;
623
+ anchor.dispatchEvent(clickEvent);
624
+ }
625
+ })
626
+ .catch(() => {
627
+ // this catch happens, when no zip was created.
628
+ // it is handled by the UI showing "FAILED" for all files
629
+ });
630
+ }
631
+ }),
632
+ };
633
+ };
634
+ return handler;
635
+ })();
412
636
 
413
637
  const DEFAULT_PAGE_SIZE$5 = 1000;
414
638
  const parseItems = (items, excludedPath) => items
@@ -494,7 +718,7 @@ const uploadHandler = ({ config, data, options }) => {
494
718
  path: key,
495
719
  data: file,
496
720
  options: {
497
- bucket: constructBucket(config),
721
+ bucket: constructBucket$1(config),
498
722
  expectedBucketOwner: accountId,
499
723
  locationCredentialsProvider: credentials,
500
724
  onProgress: (event) => {
@@ -1719,8 +1943,10 @@ const USE_LIST_ERROR_MESSAGE = '`useList` must be called from within `StorageBro
1719
1943
  const INITIAL_STATUS_COUNTS = {
1720
1944
  CANCELED: 0,
1721
1945
  COMPLETE: 0,
1946
+ LOADED: 0,
1722
1947
  FAILED: 0,
1723
1948
  PENDING: 0,
1949
+ FINISHING: 0,
1724
1950
  OVERWRITE_PREVENTED: 0,
1725
1951
  QUEUED: 0,
1726
1952
  TOTAL: 0,
@@ -1731,7 +1957,10 @@ const isProcessingTasks = (statusCounts) => {
1731
1957
  if (statusCounts.TOTAL === 0 || statusCounts.TOTAL === statusCounts.QUEUED) {
1732
1958
  return false;
1733
1959
  }
1734
- return !(statusCounts.QUEUED === 0 && statusCounts.PENDING === 0);
1960
+ return !(statusCounts.QUEUED === 0 &&
1961
+ statusCounts.PENDING === 0 &&
1962
+ statusCounts.LOADED === 0 &&
1963
+ statusCounts.FINISHING === 0);
1735
1964
  };
1736
1965
  const hasCompletedProcessingTasks = (statusCounts) => {
1737
1966
  if (statusCounts.TOTAL === 0 || isProcessingTasks(statusCounts))
@@ -1829,20 +2058,26 @@ function useProcessTasks(handler, options) {
1829
2058
  : [...tasksRef.current.values()].find(({ status }) => status === 'QUEUED') ?? {};
1830
2059
  if (!data)
1831
2060
  return;
2061
+ const all = isSingleTask
2062
+ ? [_input.data]
2063
+ : [...tasksRef.current.values()].map(({ data }) => data);
1832
2064
  const { onTaskCancel, onTaskComplete, onTaskError, onTaskProgress, onTaskSuccess, } = callbacksRef.current;
1833
2065
  const getTask = () => tasksRef.current.get(data.id);
1834
2066
  const { options } = _input;
1835
2067
  const { onProgress: _onProgress } = options ?? {};
1836
- const onProgress = ({ id }, progress) => {
1837
- const task = updateTask(id, { progress });
2068
+ const onProgress = ({ id }, progress, status = 'PENDING') => {
2069
+ const task = updateTask(id, {
2070
+ progress,
2071
+ status,
2072
+ });
1838
2073
  if (task && ui.isFunction(onTaskProgress)) {
1839
2074
  onTaskProgress(task, progress);
1840
2075
  }
1841
2076
  if (task && ui.isFunction(_onProgress)) {
1842
- _onProgress(data, progress);
2077
+ _onProgress(data, progress, status);
1843
2078
  }
1844
2079
  };
1845
- const input = { ..._input, data, options: { ...options, onProgress } };
2080
+ const input = { ..._input, data, all, options: { ...options, onProgress } };
1846
2081
  const { cancel: _cancel, result } = handler(input);
1847
2082
  const cancel = !_cancel
1848
2083
  ? undefined
@@ -1927,7 +2162,7 @@ function useHandler(handler, options) {
1927
2162
  handleProcessing({
1928
2163
  config,
1929
2164
  ...(hasData
1930
- ? { data: input.data }
2165
+ ? { data: input.data, all: [input.data] }
1931
2166
  : // if no `data` provided, provide `concurrency` to `options`
1932
2167
  { options: { concurrency: DEFAULT_ACTION_CONCURRENCY } }),
1933
2168
  });
@@ -2036,8 +2271,10 @@ const DEFAULT_ACTION_VIEW_DISPLAY_TEXT = {
2036
2271
  actionDestinationLabel: 'Destination',
2037
2272
  statusDisplayCanceledLabel: 'Canceled',
2038
2273
  statusDisplayCompletedLabel: 'Completed',
2274
+ statusDisplayLoadedLabel: 'Completed',
2039
2275
  statusDisplayFailedLabel: 'Failed',
2040
2276
  statusDisplayInProgressLabel: 'In progress',
2277
+ statusDisplayFinishingLabel: 'In progress',
2041
2278
  statusDisplayTotalLabel: 'Total',
2042
2279
  statusDisplayQueuedLabel: 'Not started',
2043
2280
  // empty by default
@@ -2390,6 +2627,8 @@ const DEFAULT_DOWNLOAD_VIEW_DISPLAY_TEXT = {
2390
2627
  ...DEFAULT_ACTION_VIEW_DISPLAY_TEXT,
2391
2628
  title: 'Download',
2392
2629
  actionStartLabel: 'Download',
2630
+ statusDisplayFinishingLabel: 'Zipping',
2631
+ statusDisplayLoadedLabel: 'Loaded',
2393
2632
  getActionCompleteMessage: (data) => {
2394
2633
  const { counts } = data ?? {};
2395
2634
  const { COMPLETE, FAILED, TOTAL } = counts ?? {};
@@ -3746,15 +3985,19 @@ function useResolveTableData(keys, { getCell, getHeader, getRowKey }, { items, p
3746
3985
 
3747
3986
  const STATUS_LABELS = {
3748
3987
  PENDING: 'statusDisplayInProgressLabel',
3988
+ FINISHING: 'statusDisplayFinishingLabel',
3749
3989
  CANCELED: 'statusDisplayCanceledLabel',
3750
3990
  COMPLETE: 'statusDisplayCompletedLabel',
3991
+ LOADED: 'statusDisplayLoadedLabel',
3751
3992
  FAILED: 'statusDisplayFailedLabel',
3752
3993
  QUEUED: 'statusDisplayQueuedLabel',
3753
3994
  OVERWRITE_PREVENTED: 'statusDisplayOverwritePreventedLabel',
3754
3995
  };
3755
3996
  const STATUS_ICONS = {
3756
3997
  PENDING: 'action-progress',
3998
+ FINISHING: 'action-progress',
3757
3999
  COMPLETE: 'action-success',
4000
+ LOADED: 'action-success',
3758
4001
  FAILED: 'action-error',
3759
4002
  OVERWRITE_PREVENTED: 'action-info',
3760
4003
  CANCELED: 'action-canceled',
@@ -3795,7 +4038,7 @@ const getUploadCellProgress = ({ progress, status, }) => {
3795
4038
  };
3796
4039
  const getDownloadCellProgress = ({ progress, status, }) => {
3797
4040
  // prefer `progress` if available, 1 if status is complete, default 0
3798
- const value = progress ?? (status === 'COMPLETE' ? 1 : 0);
4041
+ const value = progress ?? (status === 'LOADED' || status === 'COMPLETE' ? 1 : 0);
3799
4042
  const displayValue = `${Math.round(value * 100)}%`;
3800
4043
  return { displayValue, value };
3801
4044
  };
@@ -5536,7 +5779,7 @@ function useFilePreview({ activeFile, }) {
5536
5779
  const config = getConfig(location.current);
5537
5780
  const { accountId, customEndpoint, credentials } = config;
5538
5781
  const sharedOptions = {
5539
- bucket: constructBucket(config),
5782
+ bucket: constructBucket$1(config),
5540
5783
  expectedBucketOwner: accountId,
5541
5784
  };
5542
5785
  try {
@@ -6368,6 +6611,7 @@ exports.VERSION = VERSION;
6368
6611
  exports.componentsDefault = componentsDefault;
6369
6612
  exports.createAmplifyAuthAdapter = createAmplifyAuthAdapter;
6370
6613
  exports.createStorageBrowser = createStorageBrowser;
6614
+ exports.deduplicateLocations = deduplicateLocations;
6371
6615
  exports.defaultActionConfigs = defaultActionConfigs;
6372
6616
  exports.defaultHandlers = defaultHandlers;
6373
6617
  exports.getFilteredLocations = getFilteredLocations;