@aws-amplify/ui-react-storage 3.13.1 → 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 (74) hide show
  1. package/dist/browser.js +9 -3
  2. package/dist/{createStorageBrowser-DaVWyJzC.js → createStorageBrowser-CG-6mXiT.js} +487 -146
  3. package/dist/esm/browser.mjs +2 -0
  4. package/dist/esm/components/StorageBrowser/ErrorBoundary/ErrorBoundary.mjs +2 -0
  5. package/dist/esm/components/StorageBrowser/StorageBrowserAmplify.mjs +2 -0
  6. package/dist/esm/components/StorageBrowser/actions/configs/defaults.mjs +2 -0
  7. package/dist/esm/components/StorageBrowser/actions/handlers/defaults.mjs +1 -1
  8. package/dist/esm/components/StorageBrowser/actions/handlers/listLocationItems.mjs +4 -2
  9. package/dist/esm/components/StorageBrowser/actions/handlers/listLocations.mjs +7 -2
  10. package/dist/esm/components/StorageBrowser/actions/handlers/utils.mjs +87 -2
  11. package/dist/esm/components/StorageBrowser/actions/handlers/zipdownload.mjs +195 -0
  12. package/dist/esm/components/StorageBrowser/adapters/createAmplifyAuthAdapter/createAmplifyListLocationsHandler.mjs +3 -1
  13. package/dist/esm/components/StorageBrowser/adapters/createManagedAuthAdapter/createManagedAuthAdapter.mjs +2 -0
  14. package/dist/esm/components/StorageBrowser/components/ComponentsProvider.mjs +2 -0
  15. package/dist/esm/components/StorageBrowser/components/base/preview/DownloadButton.mjs +2 -0
  16. package/dist/esm/components/StorageBrowser/controls/DataTableControl.mjs +2 -0
  17. package/dist/esm/components/StorageBrowser/createStorageBrowser/StorageBrowserDefault.mjs +2 -0
  18. package/dist/esm/components/StorageBrowser/createStorageBrowser/createProvider.mjs +2 -0
  19. package/dist/esm/components/StorageBrowser/createStorageBrowser/createStorageBrowser.mjs +2 -0
  20. package/dist/esm/components/StorageBrowser/displayText/libraries/en/downloadView.mjs +3 -0
  21. package/dist/esm/components/StorageBrowser/displayText/libraries/en/shared.mjs +2 -0
  22. package/dist/esm/components/StorageBrowser/locationItems/context.mjs +1 -0
  23. package/dist/esm/components/StorageBrowser/tasks/constants.mjs +2 -0
  24. package/dist/esm/components/StorageBrowser/tasks/useProcessTasks.mjs +10 -4
  25. package/dist/esm/components/StorageBrowser/tasks/utils.mjs +4 -1
  26. package/dist/esm/components/StorageBrowser/useAction/useHandler.mjs +1 -1
  27. package/dist/esm/components/StorageBrowser/useAction/utils.mjs +2 -0
  28. package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/CopyView.mjs +2 -0
  29. package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/CopyViewProvider.mjs +2 -0
  30. package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/FoldersMessageControl.mjs +2 -0
  31. package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/useCopyView.mjs +2 -0
  32. package/dist/esm/components/StorageBrowser/views/LocationActionView/CopyView/useFolders.mjs +2 -0
  33. package/dist/esm/components/StorageBrowser/views/LocationActionView/CreateFolderView/CreateFolderView.mjs +2 -0
  34. package/dist/esm/components/StorageBrowser/views/LocationActionView/CreateFolderView/useCreateFolderView.mjs +2 -0
  35. package/dist/esm/components/StorageBrowser/views/LocationActionView/DeleteView/DeleteView.mjs +2 -0
  36. package/dist/esm/components/StorageBrowser/views/LocationActionView/DeleteView/useDeleteView.mjs +2 -0
  37. package/dist/esm/components/StorageBrowser/views/LocationActionView/DownloadView/DownloadView.mjs +2 -0
  38. package/dist/esm/components/StorageBrowser/views/LocationActionView/DownloadView/DownloadViewProvider.mjs +2 -3
  39. package/dist/esm/components/StorageBrowser/views/LocationActionView/DownloadView/useDownloadView.mjs +2 -0
  40. package/dist/esm/components/StorageBrowser/views/LocationActionView/UploadView/UploadView.mjs +2 -0
  41. package/dist/esm/components/StorageBrowser/views/LocationActionView/UploadView/UploadViewProvider.mjs +1 -0
  42. package/dist/esm/components/StorageBrowser/views/LocationActionView/UploadView/useUploadView.mjs +2 -0
  43. package/dist/esm/components/StorageBrowser/views/LocationDetailView/LocationDetailView.mjs +2 -0
  44. package/dist/esm/components/StorageBrowser/views/LocationDetailView/getLocationDetailViewTableData/getLocationDetailViewTableData.mjs +1 -0
  45. package/dist/esm/components/StorageBrowser/views/LocationDetailView/useLocationDetailView.mjs +2 -0
  46. package/dist/esm/components/StorageBrowser/views/LocationsView/LocationsView.mjs +2 -0
  47. package/dist/esm/components/StorageBrowser/views/LocationsView/LocationsViewProvider.mjs +2 -0
  48. package/dist/esm/components/StorageBrowser/views/LocationsView/useLocationsView.mjs +1 -0
  49. package/dist/esm/components/StorageBrowser/views/context/actionViews.mjs +2 -0
  50. package/dist/esm/components/StorageBrowser/views/context/primaryViews.mjs +2 -0
  51. package/dist/esm/components/StorageBrowser/views/hooks/useFilePreview/useFilePreview.mjs +1 -0
  52. package/dist/esm/components/StorageBrowser/views/utils/tableResolvers/constants.mjs +4 -0
  53. package/dist/esm/components/StorageBrowser/views/utils/tableResolvers/downloadResolvers.mjs +72 -4
  54. package/dist/esm/components/StorageBrowser/views/utils/tableResolvers/utils.mjs +7 -1
  55. package/dist/esm/version.mjs +1 -1
  56. package/dist/index.js +2 -1
  57. package/dist/styles.css +17 -0
  58. package/dist/types/components/StorageBrowser/actions/handlers/index.d.ts +1 -0
  59. package/dist/types/components/StorageBrowser/actions/handlers/types.d.ts +4 -2
  60. package/dist/types/components/StorageBrowser/actions/handlers/utils.d.ts +13 -1
  61. package/dist/types/components/StorageBrowser/actions/handlers/zipdownload.d.ts +3 -0
  62. package/dist/types/components/StorageBrowser/displayText/types.d.ts +4 -1
  63. package/dist/types/components/StorageBrowser/tasks/types.d.ts +3 -3
  64. package/dist/types/components/StorageBrowser/tasks/useProcessTasks.d.ts +1 -1
  65. package/dist/types/components/StorageBrowser/useAction/useHandler.d.ts +1 -1
  66. package/dist/types/components/StorageBrowser/views/utils/index.d.ts +1 -1
  67. package/dist/types/components/StorageBrowser/views/utils/tableResolvers/constants.d.ts +4 -0
  68. package/dist/types/components/StorageBrowser/views/utils/tableResolvers/downloadResolvers.d.ts +3 -2
  69. package/dist/types/components/StorageBrowser/views/utils/tableResolvers/index.d.ts +1 -1
  70. package/dist/types/components/StorageBrowser/views/utils/tableResolvers/types.d.ts +4 -0
  71. package/dist/types/components/StorageBrowser/views/utils/tableResolvers/utils.d.ts +2 -1
  72. package/dist/types/version.d.ts +1 -1
  73. package/package.json +11 -8
  74. package/dist/esm/components/StorageBrowser/actions/handlers/download.mjs +0 -37
@@ -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';
34
40
 
35
- const VERSION = '3.13.1';
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;
45
+
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,103 +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 constructBucket = ({ bucket: bucketName, region, }) => ({ bucketName, region });
172
- const parseAccessGrantLocation = (location) => {
173
- const { permission, scope, type } = location;
174
- if (!scope.startsWith('s3://')) {
175
- throw new Error(`Invalid scope: ${scope}`);
176
- }
177
- const id = crypto.randomUUID();
178
- // remove default path
179
- const slicedScope = scope.slice(5);
180
- let bucket, prefix;
181
- switch (type) {
182
- case 'BUCKET': {
183
- // { scope: 's3://bucket/*', type: 'BUCKET', },
184
- bucket = slicedScope.slice(0, -2);
185
- prefix = '';
186
- break;
187
- }
188
- case 'PREFIX': {
189
- // { scope: 's3://bucket/path/*', type: 'PREFIX', },
190
- bucket = slicedScope.slice(0, slicedScope.indexOf('/'));
191
- prefix = `${slicedScope.slice(bucket.length + 1, -1)}`;
192
- break;
193
- }
194
- case 'OBJECT': {
195
- // { scope: 's3://bucket/path/to/object', type: 'OBJECT', },
196
- bucket = slicedScope.slice(0, slicedScope.indexOf('/'));
197
- prefix = slicedScope.slice(bucket.length + 1);
198
- break;
199
- }
200
- default: {
201
- throw new Error(`Invalid location type: ${type}`);
202
- }
203
- }
204
- let permissions;
205
- switch (permission) {
206
- case 'READ':
207
- permissions = ['get', 'list'];
208
- break;
209
- case 'READWRITE':
210
- permissions = ['delete', 'get', 'list', 'write'];
211
- break;
212
- case 'WRITE':
213
- permissions = ['delete', 'write'];
214
- break;
215
- default:
216
- throw new Error(`Invalid location permission: ${permission}`);
217
- }
218
- return { bucket, id, permissions: permissions, prefix, type };
219
- };
220
- const isSamePermissions = (permissionsToExclude, locationPermissions) => {
221
- if (permissionsToExclude.length !== locationPermissions.length) {
222
- return false;
223
- }
224
- const sortedLocationPermissions = locationPermissions.sort();
225
- return permissionsToExclude
226
- .sort()
227
- .every((permission, index) => permission === sortedLocationPermissions[index]);
228
- };
229
- const isSameType = (typeToExclude, locationType) => typeof typeToExclude === 'string'
230
- ? typeToExclude === locationType
231
- : typeToExclude.includes(locationType);
232
- const shouldExcludeLocation = ({ permissions, type }, exclude) => {
233
- const excludedByPermssions = !!(exclude?.exactPermissions &&
234
- isSamePermissions(exclude.exactPermissions, permissions));
235
- const excludedByType = !!(exclude?.type && isSameType(exclude.type, type));
236
- return excludedByPermssions || excludedByType;
237
- };
238
- const getFilteredLocations = (locations, exclude) => locations.reduce((filteredLocations, location) => {
239
- const parsedLocation = parseAccessGrantLocation(location);
240
- const isNonFolderLikePrefix = !parsedLocation.prefix.endsWith('/') &&
241
- parsedLocation.type === 'PREFIX';
242
- if (isNonFolderLikePrefix) {
243
- return filteredLocations;
244
- }
245
- if (!shouldExcludeLocation(parsedLocation, exclude)) {
246
- filteredLocations.push(parsedLocation);
247
- }
248
- return filteredLocations;
249
- }, []);
250
- const getFileKey = (key) => key.slice(key.lastIndexOf('/') + 1, key.length);
251
- const createFileDataItem = (data) => ({
252
- ...data,
253
- fileKey: getFileKey(data.key),
254
- });
255
- const getProgress = ({ totalBytes, transferredBytes, }) => totalBytes ? transferredBytes / totalBytes : undefined;
256
- const isMultipartUpload = (file) => file.size > MULTIPART_UPLOAD_THRESHOLD_BYTES;
257
-
258
347
  const copyHandler = (input) => {
259
348
  const { config, data } = input;
260
349
  const { accountId: expectedBucketOwner, credentials, customEndpoint, } = config;
261
350
  const { key, sourceKey, lastModified, eTag } = data;
262
- const bucket = constructBucket(config);
351
+ const bucket = constructBucket$1(config);
263
352
  const source = {
264
353
  bucket,
265
354
  expectedBucketOwner,
@@ -300,7 +389,7 @@ const createFolderHandler = (input) => {
300
389
  const { accountId, credentials, customEndpoint } = config;
301
390
  const { onProgress } = options ?? {};
302
391
  const { key, preventOverwrite } = data;
303
- const bucket = constructBucket(config);
392
+ const bucket = constructBucket$1(config);
304
393
  const { result } = internals.uploadData({
305
394
  path: key,
306
395
  data: '',
@@ -339,7 +428,7 @@ const deleteHandler = ({ config, data, }) => {
339
428
  const result = internals.remove({
340
429
  path: key,
341
430
  options: {
342
- bucket: constructBucket(config),
431
+ bucket: constructBucket$1(config),
343
432
  locationCredentialsProvider: credentials,
344
433
  expectedBucketOwner: accountId,
345
434
  customEndpoint,
@@ -356,18 +445,115 @@ const deleteHandler = ({ config, data, }) => {
356
445
  return { result };
357
446
  };
358
447
 
359
- function downloadFromUrl(fileName, url) {
360
- const a = document.createElement('a');
361
- a.href = url;
362
- a.download = fileName;
363
- a.target = '_blank';
364
- document.body.appendChild(a);
365
- a.click();
366
- document.body.removeChild(a);
367
- }
368
- const downloadHandler = ({ config, data: { key }, }) => {
369
- const { accountId, credentials, customEndpoint } = config;
370
- const result = internals.getUrl({
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;
555
+ const { key } = data;
556
+ const { url } = await internals.getUrl({
371
557
  path: key,
372
558
  options: {
373
559
  bucket: constructBucket(config),
@@ -377,17 +563,76 @@ const downloadHandler = ({ config, data: { key }, }) => {
377
563
  contentDisposition: 'attachment',
378
564
  expectedBucketOwner: accountId,
379
565
  },
380
- })
381
- .then(({ url }) => {
382
- downloadFromUrl(key, url.toString());
383
- return { status: 'COMPLETE', value: { url } };
384
- })
385
- .catch((error) => {
386
- const { message } = error;
387
- return { error, message, status: 'FAILED' };
388
566
  });
389
- return { result };
390
- };
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
+ })();
391
636
 
392
637
  const DEFAULT_PAGE_SIZE$5 = 1000;
393
638
  const parseItems = (items, excludedPath) => items
@@ -421,9 +666,10 @@ const filterDotItems = (items, prefix) => items.filter((item) => {
421
666
  const parseResult = ({ excludedSubpaths, items }, prefix) => filterDotItems([...parseExcludedPaths(excludedSubpaths), ...parseItems(items, prefix)], prefix);
422
667
  const listLocationItemsHandler = async (input) => {
423
668
  const { config, prefix, options } = input;
424
- const { bucket: _bucket, credentials, customEndpoint, region, accountId, } = config;
669
+ const { bucket: _bucket, credentials, customEndpoint, region: globalRegion, accountId, } = config;
425
670
  const { exclude, delimiter, nextToken, pageSize: _pageSize = DEFAULT_PAGE_SIZE$5, ..._options } = options ?? {};
426
- const bucket = { bucketName: _bucket, region };
671
+ const bucketRegion = getBucketRegion(_bucket, globalRegion);
672
+ const bucket = { bucketName: _bucket, region: bucketRegion };
427
673
  const subpathStrategy = {
428
674
  delimiter,
429
675
  strategy: delimiter ? 'exclude' : 'include',
@@ -472,7 +718,7 @@ const uploadHandler = ({ config, data, options }) => {
472
718
  path: key,
473
719
  data: file,
474
720
  options: {
475
- bucket: constructBucket(config),
721
+ bucket: constructBucket$1(config),
476
722
  expectedBucketOwner: accountId,
477
723
  locationCredentialsProvider: credentials,
478
724
  onProgress: (event) => {
@@ -1697,8 +1943,10 @@ const USE_LIST_ERROR_MESSAGE = '`useList` must be called from within `StorageBro
1697
1943
  const INITIAL_STATUS_COUNTS = {
1698
1944
  CANCELED: 0,
1699
1945
  COMPLETE: 0,
1946
+ LOADED: 0,
1700
1947
  FAILED: 0,
1701
1948
  PENDING: 0,
1949
+ FINISHING: 0,
1702
1950
  OVERWRITE_PREVENTED: 0,
1703
1951
  QUEUED: 0,
1704
1952
  TOTAL: 0,
@@ -1709,7 +1957,10 @@ const isProcessingTasks = (statusCounts) => {
1709
1957
  if (statusCounts.TOTAL === 0 || statusCounts.TOTAL === statusCounts.QUEUED) {
1710
1958
  return false;
1711
1959
  }
1712
- return !(statusCounts.QUEUED === 0 && statusCounts.PENDING === 0);
1960
+ return !(statusCounts.QUEUED === 0 &&
1961
+ statusCounts.PENDING === 0 &&
1962
+ statusCounts.LOADED === 0 &&
1963
+ statusCounts.FINISHING === 0);
1713
1964
  };
1714
1965
  const hasCompletedProcessingTasks = (statusCounts) => {
1715
1966
  if (statusCounts.TOTAL === 0 || isProcessingTasks(statusCounts))
@@ -1807,20 +2058,26 @@ function useProcessTasks(handler, options) {
1807
2058
  : [...tasksRef.current.values()].find(({ status }) => status === 'QUEUED') ?? {};
1808
2059
  if (!data)
1809
2060
  return;
2061
+ const all = isSingleTask
2062
+ ? [_input.data]
2063
+ : [...tasksRef.current.values()].map(({ data }) => data);
1810
2064
  const { onTaskCancel, onTaskComplete, onTaskError, onTaskProgress, onTaskSuccess, } = callbacksRef.current;
1811
2065
  const getTask = () => tasksRef.current.get(data.id);
1812
2066
  const { options } = _input;
1813
2067
  const { onProgress: _onProgress } = options ?? {};
1814
- const onProgress = ({ id }, progress) => {
1815
- const task = updateTask(id, { progress });
2068
+ const onProgress = ({ id }, progress, status = 'PENDING') => {
2069
+ const task = updateTask(id, {
2070
+ progress,
2071
+ status,
2072
+ });
1816
2073
  if (task && ui.isFunction(onTaskProgress)) {
1817
2074
  onTaskProgress(task, progress);
1818
2075
  }
1819
2076
  if (task && ui.isFunction(_onProgress)) {
1820
- _onProgress(data, progress);
2077
+ _onProgress(data, progress, status);
1821
2078
  }
1822
2079
  };
1823
- const input = { ..._input, data, options: { ...options, onProgress } };
2080
+ const input = { ..._input, data, all, options: { ...options, onProgress } };
1824
2081
  const { cancel: _cancel, result } = handler(input);
1825
2082
  const cancel = !_cancel
1826
2083
  ? undefined
@@ -1905,7 +2162,7 @@ function useHandler(handler, options) {
1905
2162
  handleProcessing({
1906
2163
  config,
1907
2164
  ...(hasData
1908
- ? { data: input.data }
2165
+ ? { data: input.data, all: [input.data] }
1909
2166
  : // if no `data` provided, provide `concurrency` to `options`
1910
2167
  { options: { concurrency: DEFAULT_ACTION_CONCURRENCY } }),
1911
2168
  });
@@ -2014,8 +2271,10 @@ const DEFAULT_ACTION_VIEW_DISPLAY_TEXT = {
2014
2271
  actionDestinationLabel: 'Destination',
2015
2272
  statusDisplayCanceledLabel: 'Canceled',
2016
2273
  statusDisplayCompletedLabel: 'Completed',
2274
+ statusDisplayLoadedLabel: 'Completed',
2017
2275
  statusDisplayFailedLabel: 'Failed',
2018
2276
  statusDisplayInProgressLabel: 'In progress',
2277
+ statusDisplayFinishingLabel: 'In progress',
2019
2278
  statusDisplayTotalLabel: 'Total',
2020
2279
  statusDisplayQueuedLabel: 'Not started',
2021
2280
  // empty by default
@@ -2368,6 +2627,8 @@ const DEFAULT_DOWNLOAD_VIEW_DISPLAY_TEXT = {
2368
2627
  ...DEFAULT_ACTION_VIEW_DISPLAY_TEXT,
2369
2628
  title: 'Download',
2370
2629
  actionStartLabel: 'Download',
2630
+ statusDisplayFinishingLabel: 'Zipping',
2631
+ statusDisplayLoadedLabel: 'Loaded',
2371
2632
  getActionCompleteMessage: (data) => {
2372
2633
  const { counts } = data ?? {};
2373
2634
  const { COMPLETE, FAILED, TOTAL } = counts ?? {};
@@ -2382,6 +2643,7 @@ const DEFAULT_DOWNLOAD_VIEW_DISPLAY_TEXT = {
2382
2643
  type: 'error',
2383
2644
  };
2384
2645
  },
2646
+ tableColumnProgressHeader: 'Progress',
2385
2647
  };
2386
2648
 
2387
2649
  const DEFAULT_STORAGE_BROWSER_DISPLAY_TEXT = {
@@ -3723,15 +3985,19 @@ function useResolveTableData(keys, { getCell, getHeader, getRowKey }, { items, p
3723
3985
 
3724
3986
  const STATUS_LABELS = {
3725
3987
  PENDING: 'statusDisplayInProgressLabel',
3988
+ FINISHING: 'statusDisplayFinishingLabel',
3726
3989
  CANCELED: 'statusDisplayCanceledLabel',
3727
3990
  COMPLETE: 'statusDisplayCompletedLabel',
3991
+ LOADED: 'statusDisplayLoadedLabel',
3728
3992
  FAILED: 'statusDisplayFailedLabel',
3729
3993
  QUEUED: 'statusDisplayQueuedLabel',
3730
3994
  OVERWRITE_PREVENTED: 'statusDisplayOverwritePreventedLabel',
3731
3995
  };
3732
3996
  const STATUS_ICONS = {
3733
3997
  PENDING: 'action-progress',
3998
+ FINISHING: 'action-progress',
3734
3999
  COMPLETE: 'action-success',
4000
+ LOADED: 'action-success',
3735
4001
  FAILED: 'action-error',
3736
4002
  OVERWRITE_PREVENTED: 'action-info',
3737
4003
  CANCELED: 'action-canceled',
@@ -3770,6 +4036,12 @@ const getUploadCellProgress = ({ progress, status, }) => {
3770
4036
  const displayValue = `${Math.round(value * 100)}%`;
3771
4037
  return { displayValue, value };
3772
4038
  };
4039
+ const getDownloadCellProgress = ({ progress, status, }) => {
4040
+ // prefer `progress` if available, 1 if status is complete, default 0
4041
+ const value = progress ?? (status === 'LOADED' || status === 'COMPLETE' ? 1 : 0);
4042
+ const displayValue = `${Math.round(value * 100)}%`;
4043
+ return { displayValue, value };
4044
+ };
3773
4045
  const getFileSize = (value, fallback = '-') => (!value ? fallback : ui.humanFileSize(value, true));
3774
4046
  const getFileDataCancelCellContent = (data) => {
3775
4047
  const { item, props } = data;
@@ -3801,31 +4073,31 @@ const getFileDataCancelCellContent = (data) => {
3801
4073
  * Generates a unique key for a table cell based on the key and item id
3802
4074
  */
3803
4075
  const getFileDataCellKey = ({ key, item, }) => `${key}-${item.data.id}`;
3804
- const name$1 = (data) => {
4076
+ const name$2 = (data) => {
3805
4077
  const key = getFileDataCellKey(data);
3806
4078
  const { item } = data;
3807
4079
  const text = item.data.fileKey;
3808
4080
  const icon = STATUS_ICONS[item.status];
3809
4081
  return { key, type: 'text', content: { icon, text } };
3810
4082
  };
3811
- const folder$1 = (data) => {
4083
+ const folder$2 = (data) => {
3812
4084
  const key = getFileDataCellKey(data);
3813
4085
  const text = getFileDataCellFolder(data.item);
3814
4086
  return { key, type: 'text', content: { text } };
3815
4087
  };
3816
- const type$1 = (data) => {
4088
+ const type$2 = (data) => {
3817
4089
  const key = getFileDataCellKey(data);
3818
4090
  const { fileKey } = data.item.data;
3819
4091
  const text = getFileType(fileKey);
3820
4092
  return { key, type: 'text', content: { text } };
3821
4093
  };
3822
- const size$1 = (data) => {
4094
+ const size$2 = (data) => {
3823
4095
  const key = getFileDataCellKey(data);
3824
4096
  const { size: value } = data.item.data;
3825
4097
  const displayValue = getFileSize(value);
3826
4098
  return { key, type: 'number', content: { value, displayValue } };
3827
4099
  };
3828
- const cancel$1 = (data) => {
4100
+ const cancel$2 = (data) => {
3829
4101
  const key = getFileDataCellKey(data);
3830
4102
  const content = getFileDataCancelCellContent(data);
3831
4103
  return { key, type: 'button', content };
@@ -3841,12 +4113,12 @@ const status$3 = (data) => {
3841
4113
  return { key, type: 'text', content: { text } };
3842
4114
  };
3843
4115
  const COPY_CELL_RESOLVERS = {
3844
- name: name$1,
3845
- folder: folder$1,
3846
- type: type$1,
3847
- size: size$1,
4116
+ name: name$2,
4117
+ folder: folder$2,
4118
+ type: type$2,
4119
+ size: size$2,
3848
4120
  status: status$3,
3849
- cancel: cancel$1,
4121
+ cancel: cancel$2,
3850
4122
  /**
3851
4123
  * @deprecated
3852
4124
  *
@@ -3878,12 +4150,12 @@ const status$2 = (data) => {
3878
4150
  return { key, type: 'text', content: { text } };
3879
4151
  };
3880
4152
  const DELETE_CELL_RESOLVERS = {
3881
- name: name$1,
3882
- folder: folder$1,
3883
- type: type$1,
3884
- size: size$1,
4153
+ name: name$2,
4154
+ folder: folder$2,
4155
+ type: type$2,
4156
+ size: size$2,
3885
4157
  status: status$2,
3886
- cancel: cancel$1,
4158
+ cancel: cancel$2,
3887
4159
  /**
3888
4160
  * @deprecated
3889
4161
  *
@@ -3905,8 +4177,42 @@ const DELETE_TABLE_RESOLVERS = {
3905
4177
  getRowKey: ({ item }) => item.data.id,
3906
4178
  };
3907
4179
 
4180
+ const DOWNLOAD_TABLE_KEYS = [
4181
+ 'name',
4182
+ 'folder',
4183
+ 'type',
4184
+ 'size',
4185
+ 'status',
4186
+ 'progress',
4187
+ 'cancel',
4188
+ ];
4189
+ const getDownloadCellKey = ({ key, item, }) => `${key}-${item.data.id}`;
4190
+ const name$1 = (data) => {
4191
+ const key = getDownloadCellKey(data);
4192
+ const { item } = data;
4193
+ const text = item.data.fileKey;
4194
+ const icon = STATUS_ICONS[item.status];
4195
+ return { key, type: 'text', content: { icon, text } };
4196
+ };
4197
+ const folder$1 = (data) => {
4198
+ const key = getDownloadCellKey(data);
4199
+ const text = getFileDataCellFolder(data.item);
4200
+ return { key, type: 'text', content: { text } };
4201
+ };
4202
+ const type$1 = (data) => {
4203
+ const key = getDownloadCellKey(data);
4204
+ const { item } = data;
4205
+ const text = getFileType(getCellName(item.data.key));
4206
+ return { key, type: 'text', content: { text } };
4207
+ };
4208
+ const size$1 = (data) => {
4209
+ const key = getDownloadCellKey(data);
4210
+ const { size: value } = data.item.data;
4211
+ const displayValue = getFileSize(value);
4212
+ return { key, type: 'number', content: { value, displayValue } };
4213
+ };
3908
4214
  const status$1 = (data) => {
3909
- const key = getFileDataCellKey(data);
4215
+ const key = getDownloadCellKey(data);
3910
4216
  const { item: { status }, props: { displayText }, } = data;
3911
4217
  const statusLabelKey = STATUS_LABELS[status];
3912
4218
  const text = isDownloadViewDisplayTextKey(statusLabelKey)
@@ -3914,12 +4220,46 @@ const status$1 = (data) => {
3914
4220
  : '';
3915
4221
  return { key, type: 'text', content: { text } };
3916
4222
  };
4223
+ const progress$1 = (data) => {
4224
+ const key = getDownloadCellKey(data);
4225
+ const content = getDownloadCellProgress(data.item);
4226
+ return { key, type: 'number', content };
4227
+ };
4228
+ const cancel$1 = (data) => {
4229
+ const key = getDownloadCellKey(data);
4230
+ const { item, props } = data;
4231
+ const { cancel, status } = item;
4232
+ const { isProcessing, onTaskRemove } = props;
4233
+ const isQueued = status === 'QUEUED';
4234
+ // a task is removable prior to processing start. Including `isQueued` ensures
4235
+ // that `isRemovable` is `false` on all tasks processing complete
4236
+ const isRemovable = isQueued && !isProcessing;
4237
+ // a task is cancelable while processing is true, and the task has a cancel handler
4238
+ const isCancelable = isProcessing && !!cancel;
4239
+ const ariaLabel = `${isRemovable ? 'Remove' : 'Cancel'} item: ${getCellName(item.data.fileKey)}`;
4240
+ const isDisabled = !isRemovable && !isCancelable;
4241
+ // resolve to `undefined` if not cancelable or removable
4242
+ const onClick = !isCancelable && !isRemovable
4243
+ ? undefined
4244
+ : () => {
4245
+ if (isRemovable) {
4246
+ onTaskRemove?.(item);
4247
+ // do not run cancel handler on remove
4248
+ return;
4249
+ }
4250
+ if (isCancelable)
4251
+ cancel();
4252
+ };
4253
+ const content = { ariaLabel, isDisabled, onClick, icon: 'cancel' };
4254
+ return { key, type: 'button', content };
4255
+ };
3917
4256
  const DOWNLOAD_CELL_RESOLVERS = {
3918
4257
  name: name$1,
3919
4258
  folder: folder$1,
3920
4259
  type: type$1,
3921
4260
  size: size$1,
3922
4261
  status: status$1,
4262
+ progress: progress$1,
3923
4263
  cancel: cancel$1,
3924
4264
  };
3925
4265
  const DOWNLOAD_TABLE_RESOLVERS = {
@@ -4950,7 +5290,7 @@ function DownloadViewProvider({ children, ...props }) {
4950
5290
  const message = isProcessingComplete
4951
5291
  ? getActionCompleteMessage({ counts: statusCounts })
4952
5292
  : undefined;
4953
- const tableData = useResolveTableData(FILE_DATA_ITEM_TABLE_KEYS, DOWNLOAD_TABLE_RESOLVERS, {
5293
+ const tableData = useResolveTableData(DOWNLOAD_TABLE_KEYS, DOWNLOAD_TABLE_RESOLVERS, {
4954
5294
  items,
4955
5295
  props: { displayText, isProcessing, onTaskRemove },
4956
5296
  });
@@ -5439,7 +5779,7 @@ function useFilePreview({ activeFile, }) {
5439
5779
  const config = getConfig(location.current);
5440
5780
  const { accountId, customEndpoint, credentials } = config;
5441
5781
  const sharedOptions = {
5442
- bucket: constructBucket(config),
5782
+ bucket: constructBucket$1(config),
5443
5783
  expectedBucketOwner: accountId,
5444
5784
  };
5445
5785
  try {
@@ -6271,6 +6611,7 @@ exports.VERSION = VERSION;
6271
6611
  exports.componentsDefault = componentsDefault;
6272
6612
  exports.createAmplifyAuthAdapter = createAmplifyAuthAdapter;
6273
6613
  exports.createStorageBrowser = createStorageBrowser;
6614
+ exports.deduplicateLocations = deduplicateLocations;
6274
6615
  exports.defaultActionConfigs = defaultActionConfigs;
6275
6616
  exports.defaultHandlers = defaultHandlers;
6276
6617
  exports.getFilteredLocations = getFilteredLocations;