@uploadcare/upload-client 6.0.1-alpha.1 → 6.0.1-alpha.10

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.
@@ -157,6 +157,26 @@ const poll = ({ check, interval = DEFAULT_INTERVAL, timeout, signal }) => new Pr
157
157
  tickTimeoutId = setTimeout(tick, 0);
158
158
  });
159
159
 
160
+ /*
161
+ Settings for future support:
162
+ parallelDirectUploads: 10,
163
+ */
164
+ const defaultSettings = {
165
+ baseCDN: 'https://ucarecdn.com',
166
+ baseURL: 'https://upload.uploadcare.com',
167
+ maxContentLength: 50 * 1024 * 1024,
168
+ retryThrottledRequestMaxTimes: 1,
169
+ retryNetworkErrorMaxTimes: 3,
170
+ multipartMinFileSize: 25 * 1024 * 1024,
171
+ multipartChunkSize: 5 * 1024 * 1024,
172
+ multipartMinLastPartSize: 1024 * 1024,
173
+ maxConcurrentRequests: 4,
174
+ pollingTimeoutMilliseconds: 10000,
175
+ pusherKey: '79ae88bd931ea68464d9'
176
+ };
177
+ const defaultContentType = 'application/octet-stream';
178
+ const defaultFilename = 'original';
179
+
160
180
  const request = ({ method, url, data, headers = {}, signal, onProgress }) => new Promise((resolve, reject) => {
161
181
  const xhr = new XMLHttpRequest();
162
182
  const requestMethod = method?.toUpperCase() || 'GET';
@@ -249,45 +269,42 @@ const request = ({ method, url, data, headers = {}, signal, onProgress }) => new
249
269
  }
250
270
  });
251
271
 
252
- const getFileOptions = ({ name }) => name ? [name] : [];
253
- const transformFile = (file, name) => {
254
- if (typeof file === 'string' && file.startsWith('file:')) {
255
- return { uri: file, name };
256
- }
257
- const uri = URL.createObjectURL(file);
258
- const type = file.type;
259
- return { uri, name, type };
272
+ const isBlob = (data) => {
273
+ return typeof Blob !== 'undefined' && data instanceof Blob;
260
274
  };
261
- var getFormData = () => new FormData();
262
-
263
- /**
264
- * FileData type guard.
265
- */
266
- const isFileData = (data) => {
267
- return (data !== undefined &&
268
- ((typeof Blob !== 'undefined' && data instanceof Blob) ||
269
- (typeof File !== 'undefined' && data instanceof File) ||
270
- (typeof Buffer !== 'undefined' && data instanceof Buffer) ||
271
- (typeof data === 'string' && data.startsWith('file:'))));
275
+ const isFile = (data) => {
276
+ return typeof File !== 'undefined' && data instanceof File;
272
277
  };
273
- /**
274
- * Uuid type guard.
275
- */
276
- const isUuid = (data) => {
277
- const UUID_REGEX = '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}';
278
- const regExp = new RegExp(UUID_REGEX);
279
- return !isFileData(data) && regExp.test(data);
278
+ const isBuffer = (data) => {
279
+ return typeof Buffer !== 'undefined' && data instanceof Buffer;
280
280
  };
281
- /**
282
- * Url type guard.
283
- *
284
- * @param {NodeFile | BrowserFile | Url | Uuid} data
285
- */
286
- const isUrl = (data) => {
287
- const URL_REGEX = '^(?:\\w+:)?\\/\\/([^\\s\\.]+\\.\\S{2}|localhost[\\:?\\d]*)\\S*$';
288
- const regExp = new RegExp(URL_REGEX);
289
- return !isFileData(data) && regExp.test(data);
281
+ const isReactNativeAsset = (data) => {
282
+ return (!!data &&
283
+ typeof data === 'object' &&
284
+ !Array.isArray(data) &&
285
+ 'uri' in data &&
286
+ typeof data.uri === 'string');
287
+ };
288
+ const isFileData = (data) => {
289
+ return (isBlob(data) || isFile(data) || isBuffer(data) || isReactNativeAsset(data));
290
+ };
291
+
292
+ const getFileOptions = () => [];
293
+ const transformFile = (file, name, contentType) => {
294
+ if (isReactNativeAsset(file)) {
295
+ return {
296
+ uri: file.uri,
297
+ name: file.name || name,
298
+ type: file.type || contentType
299
+ };
300
+ }
301
+ if (isBlob(file)) {
302
+ const uri = URL.createObjectURL(file);
303
+ return { uri, name: name, type: file.type || contentType };
304
+ }
305
+ throw new Error(`Unsupported file type: ${file}`);
290
306
  };
307
+ var getFormData = () => new FormData();
291
308
 
292
309
  const isSimpleValue = (value) => {
293
310
  return (typeof value === 'string' ||
@@ -304,8 +321,8 @@ const isFileValue = (value) => !!value &&
304
321
  function collectParams(params, inputKey, inputValue) {
305
322
  if (isFileValue(inputValue)) {
306
323
  const { name, contentType } = inputValue;
307
- const file = transformFile(inputValue.data, name); // lgtm [js/superfluous-trailing-arguments]
308
- const options = getFileOptions({ name, contentType });
324
+ const file = transformFile(inputValue.data, name, contentType); // lgtm [js/superfluous-trailing-arguments]
325
+ const options = getFileOptions();
309
326
  params.push([inputKey, file, ...options]);
310
327
  }
311
328
  else if (isObjectValue(inputValue)) {
@@ -337,6 +354,19 @@ function buildFormData(options) {
337
354
  return formData;
338
355
  }
339
356
 
357
+ class UploadClientError extends Error {
358
+ constructor(message, code, request, response, headers) {
359
+ super();
360
+ this.name = 'UploadClientError';
361
+ this.message = message;
362
+ this.code = code;
363
+ this.request = request;
364
+ this.response = response;
365
+ this.headers = headers;
366
+ Object.setPrototypeOf(this, UploadClientError.prototype);
367
+ }
368
+ }
369
+
340
370
  const buildSearchParams = (query) => {
341
371
  const searchParams = new URLSearchParams();
342
372
  for (const [key, value] of Object.entries(query)) {
@@ -368,26 +398,6 @@ const getUrl = (base, path, query) => {
368
398
  return url.toString();
369
399
  };
370
400
 
371
- /*
372
- Settings for future support:
373
- parallelDirectUploads: 10,
374
- */
375
- const defaultSettings = {
376
- baseCDN: 'https://ucarecdn.com',
377
- baseURL: 'https://upload.uploadcare.com',
378
- maxContentLength: 50 * 1024 * 1024,
379
- retryThrottledRequestMaxTimes: 1,
380
- retryNetworkErrorMaxTimes: 3,
381
- multipartMinFileSize: 25 * 1024 * 1024,
382
- multipartChunkSize: 5 * 1024 * 1024,
383
- multipartMinLastPartSize: 1024 * 1024,
384
- maxConcurrentRequests: 4,
385
- pollingTimeoutMilliseconds: 10000,
386
- pusherKey: '79ae88bd931ea68464d9'
387
- };
388
- const defaultContentType = 'application/octet-stream';
389
- const defaultFilename = 'original';
390
-
391
401
  var version = '6.0.0';
392
402
 
393
403
  const LIBRARY_NAME = 'UploadcareUploadClient';
@@ -400,19 +410,6 @@ function getUserAgent(options) {
400
410
  });
401
411
  }
402
412
 
403
- class UploadClientError extends Error {
404
- constructor(message, code, request, response, headers) {
405
- super();
406
- this.name = 'UploadClientError';
407
- this.message = message;
408
- this.code = code;
409
- this.request = request;
410
- this.response = response;
411
- this.headers = headers;
412
- Object.setPrototypeOf(this, UploadClientError.prototype);
413
- }
414
- }
415
-
416
413
  const REQUEST_WAS_THROTTLED_CODE = 'RequestThrottledError';
417
414
  const DEFAULT_RETRY_AFTER_TIMEOUT = 15000;
418
415
  const DEFAULT_NETWORK_ERROR_TIMEOUT = 1000;
@@ -443,6 +440,36 @@ function retryIfFailed(fn, options) {
443
440
  }));
444
441
  }
445
442
 
443
+ const getContentType = (file) => {
444
+ let contentType = '';
445
+ if (isBlob(file) || isFile(file) || isReactNativeAsset(file)) {
446
+ contentType = file.type;
447
+ }
448
+ if (contentType) {
449
+ return contentType;
450
+ }
451
+ console.warn(`Cannot determine content type for ${file}. Using default content type: ${defaultContentType}`);
452
+ return defaultContentType;
453
+ };
454
+
455
+ const getFilename = (file) => {
456
+ let filename = '';
457
+ if (isBlob(file) || isBuffer(file)) {
458
+ filename = '';
459
+ }
460
+ else if (isFile(file) && file.name) {
461
+ filename = file.name;
462
+ }
463
+ else if (isReactNativeAsset(file) && file.name) {
464
+ filename = file.name;
465
+ }
466
+ if (filename) {
467
+ return filename;
468
+ }
469
+ console.warn(`Cannot determine filename for ${file}. Using default filename: ${defaultFilename}`);
470
+ return defaultFilename;
471
+ };
472
+
446
473
  function getStoreValue(store) {
447
474
  return typeof store === 'undefined' ? 'auto' : store ? '1' : '0';
448
475
  }
@@ -463,8 +490,8 @@ function base(file, { publicKey, fileName, contentType, baseURL = defaultSetting
463
490
  data: buildFormData({
464
491
  file: {
465
492
  data: file,
466
- name: fileName ?? file?.name ?? defaultFilename,
467
- contentType
493
+ name: fileName || getFilename(file),
494
+ contentType: contentType || getContentType(file)
468
495
  },
469
496
  UPLOADCARE_PUB_KEY: publicKey,
470
497
  UPLOADCARE_STORE: getStoreValue(store),
@@ -662,9 +689,9 @@ function multipartStart(size, { publicKey, contentType, fileName, multipartChunk
662
689
  'X-UC-User-Agent': getUserAgent({ publicKey, integration, userAgent })
663
690
  },
664
691
  data: buildFormData({
665
- filename: fileName ?? defaultFilename,
692
+ filename: fileName || defaultFilename,
666
693
  size: size,
667
- content_type: contentType ?? defaultContentType,
694
+ content_type: contentType || defaultContentType,
668
695
  part_size: multipartChunkSize,
669
696
  UPLOADCARE_STORE: getStoreValue(store),
670
697
  UPLOADCARE_PUB_KEY: publicKey,
@@ -741,6 +768,28 @@ function multipartComplete(uuid, { publicKey, baseURL = defaultSettings.baseURL,
741
768
  }), { retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes });
742
769
  }
743
770
 
771
+ function isReadyPoll({ file, publicKey, baseURL, source, integration, userAgent, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes, signal, onProgress }) {
772
+ return poll({
773
+ check: (signal) => info(file, {
774
+ publicKey,
775
+ baseURL,
776
+ signal,
777
+ source,
778
+ integration,
779
+ userAgent,
780
+ retryThrottledRequestMaxTimes,
781
+ retryNetworkErrorMaxTimes
782
+ }).then((response) => {
783
+ if (response.isReady) {
784
+ return response;
785
+ }
786
+ onProgress && onProgress({ isComputable: true, value: 1 });
787
+ return false;
788
+ }),
789
+ signal
790
+ });
791
+ }
792
+
744
793
  class UploadcareFile {
745
794
  constructor(fileInfo, { baseCDN, fileName }) {
746
795
  this.name = null;
@@ -778,28 +827,6 @@ class UploadcareFile {
778
827
  }
779
828
  }
780
829
 
781
- function isReadyPoll({ file, publicKey, baseURL, source, integration, userAgent, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes, signal, onProgress }) {
782
- return poll({
783
- check: (signal) => info(file, {
784
- publicKey,
785
- baseURL,
786
- signal,
787
- source,
788
- integration,
789
- userAgent,
790
- retryThrottledRequestMaxTimes,
791
- retryNetworkErrorMaxTimes
792
- }).then((response) => {
793
- if (response.isReady) {
794
- return response;
795
- }
796
- onProgress && onProgress({ isComputable: true, value: 1 });
797
- return false;
798
- }),
799
- signal
800
- });
801
- }
802
-
803
830
  const uploadDirect = (file, { publicKey, fileName, baseURL, secureSignature, secureExpire, store, contentType, signal, onProgress, source, integration, userAgent, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes, baseCDN, metadata }) => {
804
831
  return base(file, {
805
832
  publicKey,
@@ -835,6 +862,29 @@ const uploadDirect = (file, { publicKey, fileName, baseURL, secureSignature, sec
835
862
  .then((fileInfo) => new UploadcareFile(fileInfo, { baseCDN }));
836
863
  };
837
864
 
865
+ const uploadFromUploaded = (uuid, { publicKey, fileName, baseURL, signal, onProgress, source, integration, userAgent, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes, baseCDN }) => {
866
+ return info(uuid, {
867
+ publicKey,
868
+ baseURL,
869
+ signal,
870
+ source,
871
+ integration,
872
+ userAgent,
873
+ retryThrottledRequestMaxTimes,
874
+ retryNetworkErrorMaxTimes
875
+ })
876
+ .then((fileInfo) => new UploadcareFile(fileInfo, { baseCDN, fileName }))
877
+ .then((result) => {
878
+ // hack for node ¯\_(ツ)_/¯
879
+ if (onProgress)
880
+ onProgress({
881
+ isComputable: true,
882
+ value: 1
883
+ });
884
+ return result;
885
+ });
886
+ };
887
+
838
888
  const race = (fns, { signal } = {}) => {
839
889
  let lastError = null;
840
890
  let winnerIndex = null;
@@ -1174,35 +1224,30 @@ const uploadFromUrl = (sourceUrl, { publicKey, fileName, baseURL, baseCDN, check
1174
1224
  }))
1175
1225
  .then((fileInfo) => new UploadcareFile(fileInfo, { baseCDN }));
1176
1226
 
1177
- const uploadFromUploaded = (uuid, { publicKey, fileName, baseURL, signal, onProgress, source, integration, userAgent, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes, baseCDN }) => {
1178
- return info(uuid, {
1179
- publicKey,
1180
- baseURL,
1181
- signal,
1182
- source,
1183
- integration,
1184
- userAgent,
1185
- retryThrottledRequestMaxTimes,
1186
- retryNetworkErrorMaxTimes
1187
- })
1188
- .then((fileInfo) => new UploadcareFile(fileInfo, { baseCDN, fileName }))
1189
- .then((result) => {
1190
- // hack for node ¯\_(ツ)_/¯
1191
- if (onProgress)
1192
- onProgress({
1193
- isComputable: true,
1194
- value: 1
1195
- });
1196
- return result;
1197
- });
1227
+ const memo = new WeakMap();
1228
+ const getReactNativeBlob = async (asset) => {
1229
+ if (memo.has(asset)) {
1230
+ return memo.get(asset);
1231
+ }
1232
+ const blob = await fetch(asset.uri).then((res) => res.blob());
1233
+ memo.set(asset, blob);
1234
+ return blob;
1198
1235
  };
1199
1236
 
1200
- /**
1201
- * Get file size.
1202
- */
1203
- const getFileSize = (file) => {
1204
- return file.length || file.size;
1237
+ const getFileSize = async (file) => {
1238
+ if (isBuffer(file)) {
1239
+ return file.length;
1240
+ }
1241
+ if (isFile(file) || isBlob(file)) {
1242
+ return file.size;
1243
+ }
1244
+ if (isReactNativeAsset(file)) {
1245
+ const blob = await getReactNativeBlob(file);
1246
+ return blob.size;
1247
+ }
1248
+ throw new Error(`Failed to get file size for file: ${file}`);
1205
1249
  };
1250
+
1206
1251
  /**
1207
1252
  * Check if FileData is multipart data.
1208
1253
  */
@@ -1210,28 +1255,24 @@ const isMultipart = (fileSize, multipartMinFileSize = defaultSettings.multipartM
1210
1255
  return fileSize >= multipartMinFileSize;
1211
1256
  };
1212
1257
 
1213
- const sliceChunk = (file, index, fileSize, chunkSize) => {
1214
- const start = chunkSize * index;
1215
- const end = Math.min(start + chunkSize, fileSize);
1216
- return file.slice(start, end);
1258
+ /**
1259
+ * Uuid type guard.
1260
+ */
1261
+ const isUuid = (data) => {
1262
+ const UUID_REGEX = '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}';
1263
+ const regExp = new RegExp(UUID_REGEX);
1264
+ return !isFileData(data) && regExp.test(data);
1217
1265
  };
1218
-
1219
1266
  /**
1220
- * React-native hack for blob slicing
1221
- *
1222
- * We need to store references to sliced blobs to prevent source blob from
1223
- * being deallocated until uploading complete. Access to deallocated blob
1224
- * causes app crash.
1267
+ * Url type guard.
1225
1268
  *
1226
- * See https://github.com/uploadcare/uploadcare-js-api-clients/issues/306
1227
- * and https://github.com/facebook/react-native/issues/27543
1269
+ * @param {AnyFile | Url | Uuid} data
1228
1270
  */
1229
- function prepareChunks(file, fileSize, chunkSize) {
1230
- return (index) => {
1231
- const chunk = sliceChunk(file, index, fileSize, chunkSize);
1232
- return chunk;
1233
- };
1234
- }
1271
+ const isUrl = (data) => {
1272
+ const URL_REGEX = '^(?:\\w+:)?\\/\\/([^\\s\\.]+\\.\\S{2}|localhost[\\:?\\d]*)\\S*$';
1273
+ const regExp = new RegExp(URL_REGEX);
1274
+ return !isFileData(data) && regExp.test(data);
1275
+ };
1235
1276
 
1236
1277
  const runWithConcurrency = (concurrency, tasks) => {
1237
1278
  return new Promise((resolve, reject) => {
@@ -1268,6 +1309,36 @@ const runWithConcurrency = (concurrency, tasks) => {
1268
1309
  });
1269
1310
  };
1270
1311
 
1312
+ const sliceChunk = (file, index, fileSize, chunkSize) => {
1313
+ const start = chunkSize * index;
1314
+ const end = Math.min(start + chunkSize, fileSize);
1315
+ return file.slice(start, end);
1316
+ };
1317
+
1318
+ /**
1319
+ * React-native hack for blob slicing
1320
+ *
1321
+ * We need to store references to sliced blobs to prevent source blob from
1322
+ * being deallocated until uploading complete. Access to deallocated blob
1323
+ * causes app crash.
1324
+ *
1325
+ * See https://github.com/uploadcare/uploadcare-js-api-clients/issues/306
1326
+ * and https://github.com/facebook/react-native/issues/27543
1327
+ */
1328
+ const prepareChunks = async (file, fileSize, chunkSize) => {
1329
+ let blob;
1330
+ if (isReactNativeAsset(file)) {
1331
+ blob = await getReactNativeBlob(file);
1332
+ }
1333
+ else {
1334
+ blob = file;
1335
+ }
1336
+ return (index) => {
1337
+ const chunk = sliceChunk(blob, index, fileSize, chunkSize);
1338
+ return chunk;
1339
+ };
1340
+ };
1341
+
1271
1342
  const uploadPart = (chunk, url, { publicKey, onProgress, signal, integration, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes }) => multipartUpload(chunk, url, {
1272
1343
  publicKey,
1273
1344
  onProgress,
@@ -1276,8 +1347,8 @@ const uploadPart = (chunk, url, { publicKey, onProgress, signal, integration, re
1276
1347
  retryThrottledRequestMaxTimes,
1277
1348
  retryNetworkErrorMaxTimes
1278
1349
  });
1279
- const uploadMultipart = (file, { publicKey, fileName, fileSize, baseURL, secureSignature, secureExpire, store, signal, onProgress, source, integration, userAgent, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes, contentType, multipartChunkSize = defaultSettings.multipartChunkSize, maxConcurrentRequests = defaultSettings.maxConcurrentRequests, baseCDN, metadata }) => {
1280
- const size = fileSize || getFileSize(file);
1350
+ const uploadMultipart = async (file, { publicKey, fileName, fileSize, baseURL, secureSignature, secureExpire, store, signal, onProgress, source, integration, userAgent, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes, contentType, multipartChunkSize = defaultSettings.multipartChunkSize, maxConcurrentRequests = defaultSettings.maxConcurrentRequests, baseCDN, metadata }) => {
1351
+ const size = fileSize || await getFileSize(file);
1281
1352
  let progressValues;
1282
1353
  const createProgressHandler = (totalChunks, chunkIdx) => {
1283
1354
  if (!onProgress)
@@ -1299,8 +1370,8 @@ const uploadMultipart = (file, { publicKey, fileName, fileSize, baseURL, secureS
1299
1370
  };
1300
1371
  return multipartStart(size, {
1301
1372
  publicKey,
1302
- contentType,
1303
- fileName: fileName ?? file.name,
1373
+ contentType: contentType || getContentType(file),
1374
+ fileName: fileName || getFilename(file),
1304
1375
  baseURL,
1305
1376
  secureSignature,
1306
1377
  secureExpire,
@@ -1313,8 +1384,8 @@ const uploadMultipart = (file, { publicKey, fileName, fileSize, baseURL, secureS
1313
1384
  retryNetworkErrorMaxTimes,
1314
1385
  metadata
1315
1386
  })
1316
- .then(({ uuid, parts }) => {
1317
- const getChunk = prepareChunks(file, size, multipartChunkSize);
1387
+ .then(async ({ uuid, parts }) => {
1388
+ const getChunk = await prepareChunks(file, size, multipartChunkSize);
1318
1389
  return Promise.all([
1319
1390
  uuid,
1320
1391
  runWithConcurrency(maxConcurrentRequests, parts.map((url, index) => () => uploadPart(getChunk(index), url, {
@@ -1361,14 +1432,15 @@ const uploadMultipart = (file, { publicKey, fileName, fileSize, baseURL, secureS
1361
1432
  /**
1362
1433
  * Uploads file from provided data.
1363
1434
  */
1364
- function uploadFile(data, { publicKey, fileName, baseURL = defaultSettings.baseURL, secureSignature, secureExpire, store, signal, onProgress, source, integration, userAgent, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes, contentType, multipartMinFileSize, multipartChunkSize, maxConcurrentRequests, baseCDN = defaultSettings.baseCDN, checkForUrlDuplicates, saveUrlForRecurrentUploads, pusherKey, metadata }) {
1435
+ async function uploadFile(data, { publicKey, fileName, baseURL = defaultSettings.baseURL, secureSignature, secureExpire, store, signal, onProgress, source, integration, userAgent, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes, contentType, multipartMinFileSize, multipartChunkSize, maxConcurrentRequests, baseCDN = defaultSettings.baseCDN, checkForUrlDuplicates, saveUrlForRecurrentUploads, pusherKey, metadata }) {
1365
1436
  if (isFileData(data)) {
1366
- const fileSize = getFileSize(data);
1437
+ const fileSize = await getFileSize(data);
1367
1438
  if (isMultipart(fileSize, multipartMinFileSize)) {
1368
1439
  return uploadMultipart(data, {
1369
1440
  publicKey,
1370
1441
  contentType,
1371
1442
  multipartChunkSize,
1443
+ fileSize,
1372
1444
  fileName,
1373
1445
  baseURL,
1374
1446
  secureSignature,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uploadcare/upload-client",
3
- "version": "6.0.1-alpha.1",
3
+ "version": "6.0.1-alpha.10",
4
4
  "description": "Library for work with Uploadcare Upload API",
5
5
  "type": "module",
6
6
  "module": "./dist/index.node.js",