@uploadcare/upload-client 6.0.1-alpha.8 → 6.1.0-alpha.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.
@@ -1,7 +1,7 @@
1
1
  import http from 'http';
2
2
  import https from 'https';
3
- import { parse } from 'url';
4
3
  import { Transform, Readable } from 'stream';
4
+ import { parse } from 'url';
5
5
  import NodeFormData from 'form-data';
6
6
  import WebSocket from 'ws';
7
7
 
@@ -164,6 +164,26 @@ const poll = ({ check, interval = DEFAULT_INTERVAL, timeout, signal }) => new Pr
164
164
  tickTimeoutId = setTimeout(tick, 0);
165
165
  });
166
166
 
167
+ /*
168
+ Settings for future support:
169
+ parallelDirectUploads: 10,
170
+ */
171
+ const defaultSettings = {
172
+ baseCDN: 'https://ucarecdn.com',
173
+ baseURL: 'https://upload.uploadcare.com',
174
+ maxContentLength: 50 * 1024 * 1024,
175
+ retryThrottledRequestMaxTimes: 1,
176
+ retryNetworkErrorMaxTimes: 3,
177
+ multipartMinFileSize: 25 * 1024 * 1024,
178
+ multipartChunkSize: 5 * 1024 * 1024,
179
+ multipartMinLastPartSize: 1024 * 1024,
180
+ maxConcurrentRequests: 4,
181
+ pollingTimeoutMilliseconds: 10000,
182
+ pusherKey: '79ae88bd931ea68464d9'
183
+ };
184
+ const defaultContentType = 'application/octet-stream';
185
+ const defaultFilename = 'original';
186
+
167
187
  // ProgressEmitter is a simple PassThrough-style transform stream which keeps
168
188
  // track of the number of bytes which have been piped through it and will
169
189
  // invoke the `onprogress` function whenever new number are available.
@@ -268,7 +288,8 @@ const request = (params) => {
268
288
  }));
269
289
  };
270
290
 
271
- function identity(obj) {
291
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
292
+ function identity(obj, ..._args) {
272
293
  return obj;
273
294
  }
274
295
 
@@ -285,49 +306,24 @@ const getFileOptions = ({ name, contentType }) => [
285
306
  const transformFile = identity;
286
307
  var getFormData = () => new NodeFormData();
287
308
 
288
- const isReactNativeUri = (uri) => {
289
- if (typeof uri !== 'string') {
290
- return false;
291
- }
292
- return uri.startsWith('file:') || uri.startsWith('content:');
309
+ const isBlob = (data) => {
310
+ return typeof Blob !== 'undefined' && data instanceof Blob;
293
311
  };
294
- const isReactNativeAsset = (asset) => {
295
- return (!!asset &&
296
- typeof asset === 'object' &&
297
- !Array.isArray(asset) &&
298
- 'uri' in asset &&
299
- typeof asset.uri === 'string' &&
300
- isReactNativeUri(asset.uri));
312
+ const isFile = (data) => {
313
+ return typeof File !== 'undefined' && data instanceof File;
301
314
  };
302
-
303
- /**
304
- * FileData type guard.
305
- */
306
- const isFileData = (data) => {
307
- return (data !== undefined &&
308
- ((typeof Blob !== 'undefined' && data instanceof Blob) ||
309
- (typeof File !== 'undefined' && data instanceof File) ||
310
- (typeof Buffer !== 'undefined' && data instanceof Buffer) ||
311
- (typeof data === 'string' && isReactNativeUri(data)) ||
312
- (typeof data === 'object' && isReactNativeAsset(data))));
315
+ const isBuffer = (data) => {
316
+ return typeof Buffer !== 'undefined' && data instanceof Buffer;
313
317
  };
314
- /**
315
- * Uuid type guard.
316
- */
317
- const isUuid = (data) => {
318
- const UUID_REGEX = '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}';
319
- const regExp = new RegExp(UUID_REGEX);
320
- return !isFileData(data) && regExp.test(data);
318
+ const isReactNativeAsset = (data) => {
319
+ return (!!data &&
320
+ typeof data === 'object' &&
321
+ !Array.isArray(data) &&
322
+ 'uri' in data &&
323
+ typeof data.uri === 'string');
321
324
  };
322
- /**
323
- * Url type guard.
324
- *
325
- * @param {NodeFile | BrowserFile | Url | Uuid} data
326
- */
327
- const isUrl = (data) => {
328
- const URL_REGEX = '^(?:\\w+:)?\\/\\/([^\\s\\.]+\\.\\S{2}|localhost[\\:?\\d]*)\\S*$';
329
- const regExp = new RegExp(URL_REGEX);
330
- return !isFileData(data) && regExp.test(data);
325
+ const isFileData = (data) => {
326
+ return (isBlob(data) || isFile(data) || isBuffer(data) || isReactNativeAsset(data));
331
327
  };
332
328
 
333
329
  const isSimpleValue = (value) => {
@@ -345,7 +341,7 @@ const isFileValue = (value) => !!value &&
345
341
  function collectParams(params, inputKey, inputValue) {
346
342
  if (isFileValue(inputValue)) {
347
343
  const { name, contentType } = inputValue;
348
- const file = transformFile(inputValue.data); // lgtm [js/superfluous-trailing-arguments]
344
+ const file = transformFile(inputValue.data, name, contentType);
349
345
  const options = getFileOptions({ name, contentType });
350
346
  params.push([inputKey, file, ...options]);
351
347
  }
@@ -368,11 +364,9 @@ function getFormDataParams(options) {
368
364
  return params;
369
365
  }
370
366
  function buildFormData(options) {
371
- console.log('buildFormData', options);
372
367
  const formData = getFormData();
373
368
  const paramsList = getFormDataParams(options);
374
369
  for (const params of paramsList) {
375
- console.log('params', params);
376
370
  const [key, value, ...rest] = params;
377
371
  // node form-data has another signature for append
378
372
  formData.append(key, value, ...rest);
@@ -380,6 +374,19 @@ function buildFormData(options) {
380
374
  return formData;
381
375
  }
382
376
 
377
+ class UploadClientError extends Error {
378
+ constructor(message, code, request, response, headers) {
379
+ super();
380
+ this.name = 'UploadClientError';
381
+ this.message = message;
382
+ this.code = code;
383
+ this.request = request;
384
+ this.response = response;
385
+ this.headers = headers;
386
+ Object.setPrototypeOf(this, UploadClientError.prototype);
387
+ }
388
+ }
389
+
383
390
  const buildSearchParams = (query) => {
384
391
  const searchParams = new URLSearchParams();
385
392
  for (const [key, value] of Object.entries(query)) {
@@ -411,26 +418,6 @@ const getUrl = (base, path, query) => {
411
418
  return url.toString();
412
419
  };
413
420
 
414
- /*
415
- Settings for future support:
416
- parallelDirectUploads: 10,
417
- */
418
- const defaultSettings = {
419
- baseCDN: 'https://ucarecdn.com',
420
- baseURL: 'https://upload.uploadcare.com',
421
- maxContentLength: 50 * 1024 * 1024,
422
- retryThrottledRequestMaxTimes: 1,
423
- retryNetworkErrorMaxTimes: 3,
424
- multipartMinFileSize: 25 * 1024 * 1024,
425
- multipartChunkSize: 5 * 1024 * 1024,
426
- multipartMinLastPartSize: 1024 * 1024,
427
- maxConcurrentRequests: 4,
428
- pollingTimeoutMilliseconds: 10000,
429
- pusherKey: '79ae88bd931ea68464d9'
430
- };
431
- const defaultContentType = 'application/octet-stream';
432
- const defaultFilename = 'original';
433
-
434
421
  var version = '6.0.0';
435
422
 
436
423
  const LIBRARY_NAME = 'UploadcareUploadClient';
@@ -443,19 +430,6 @@ function getUserAgent(options) {
443
430
  });
444
431
  }
445
432
 
446
- class UploadClientError extends Error {
447
- constructor(message, code, request, response, headers) {
448
- super();
449
- this.name = 'UploadClientError';
450
- this.message = message;
451
- this.code = code;
452
- this.request = request;
453
- this.response = response;
454
- this.headers = headers;
455
- Object.setPrototypeOf(this, UploadClientError.prototype);
456
- }
457
- }
458
-
459
433
  const REQUEST_WAS_THROTTLED_CODE = 'RequestThrottledError';
460
434
  const DEFAULT_RETRY_AFTER_TIMEOUT = 15000;
461
435
  const DEFAULT_NETWORK_ERROR_TIMEOUT = 1000;
@@ -486,6 +460,36 @@ function retryIfFailed(fn, options) {
486
460
  }));
487
461
  }
488
462
 
463
+ const getContentType = (file) => {
464
+ let contentType = '';
465
+ if (isBlob(file) || isFile(file) || isReactNativeAsset(file)) {
466
+ contentType = file.type;
467
+ }
468
+ if (contentType) {
469
+ return contentType;
470
+ }
471
+ console.warn(`Cannot determine content type. Using default content type: ${defaultContentType}`, file);
472
+ return defaultContentType;
473
+ };
474
+
475
+ const getFileName = (file) => {
476
+ let filename = '';
477
+ if (isFile(file) && file.name) {
478
+ filename = file.name;
479
+ }
480
+ else if (isBlob(file) || isBuffer(file)) {
481
+ filename = '';
482
+ }
483
+ else if (isReactNativeAsset(file) && file.name) {
484
+ filename = file.name;
485
+ }
486
+ if (filename) {
487
+ return filename;
488
+ }
489
+ console.warn(`Cannot determine filename. Using default filename: ${defaultFilename}`, file);
490
+ return defaultFilename;
491
+ };
492
+
489
493
  function getStoreValue(store) {
490
494
  return typeof store === 'undefined' ? 'auto' : store ? '1' : '0';
491
495
  }
@@ -501,13 +505,13 @@ function base(file, { publicKey, fileName, contentType, baseURL = defaultSetting
501
505
  jsonerrors: 1
502
506
  }),
503
507
  headers: {
504
- 'X-UC-User-Agent': getUserAgent({ publicKey, integration, userAgent }),
508
+ 'X-UC-User-Agent': getUserAgent({ publicKey, integration, userAgent })
505
509
  },
506
510
  data: buildFormData({
507
511
  file: {
508
512
  data: file,
509
- name: fileName ?? file?.name ?? defaultFilename,
510
- contentType: contentType ?? file?.type ?? defaultContentType
513
+ name: fileName || getFileName(file),
514
+ contentType: contentType || getContentType(file)
511
515
  },
512
516
  UPLOADCARE_PUB_KEY: publicKey,
513
517
  UPLOADCARE_STORE: getStoreValue(store),
@@ -705,9 +709,9 @@ function multipartStart(size, { publicKey, contentType, fileName, multipartChunk
705
709
  'X-UC-User-Agent': getUserAgent({ publicKey, integration, userAgent })
706
710
  },
707
711
  data: buildFormData({
708
- filename: fileName ?? defaultFilename,
712
+ filename: fileName || defaultFilename,
709
713
  size: size,
710
- content_type: contentType ?? defaultContentType,
714
+ content_type: contentType || defaultContentType,
711
715
  part_size: multipartChunkSize,
712
716
  UPLOADCARE_STORE: getStoreValue(store),
713
717
  UPLOADCARE_PUB_KEY: publicKey,
@@ -784,6 +788,28 @@ function multipartComplete(uuid, { publicKey, baseURL = defaultSettings.baseURL,
784
788
  }), { retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes });
785
789
  }
786
790
 
791
+ function isReadyPoll({ file, publicKey, baseURL, source, integration, userAgent, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes, signal, onProgress }) {
792
+ return poll({
793
+ check: (signal) => info(file, {
794
+ publicKey,
795
+ baseURL,
796
+ signal,
797
+ source,
798
+ integration,
799
+ userAgent,
800
+ retryThrottledRequestMaxTimes,
801
+ retryNetworkErrorMaxTimes
802
+ }).then((response) => {
803
+ if (response.isReady) {
804
+ return response;
805
+ }
806
+ onProgress && onProgress({ isComputable: true, value: 1 });
807
+ return false;
808
+ }),
809
+ signal
810
+ });
811
+ }
812
+
787
813
  class UploadcareFile {
788
814
  constructor(fileInfo, { baseCDN, fileName }) {
789
815
  this.name = null;
@@ -821,28 +847,6 @@ class UploadcareFile {
821
847
  }
822
848
  }
823
849
 
824
- function isReadyPoll({ file, publicKey, baseURL, source, integration, userAgent, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes, signal, onProgress }) {
825
- return poll({
826
- check: (signal) => info(file, {
827
- publicKey,
828
- baseURL,
829
- signal,
830
- source,
831
- integration,
832
- userAgent,
833
- retryThrottledRequestMaxTimes,
834
- retryNetworkErrorMaxTimes
835
- }).then((response) => {
836
- if (response.isReady) {
837
- return response;
838
- }
839
- onProgress && onProgress({ isComputable: true, value: 1 });
840
- return false;
841
- }),
842
- signal
843
- });
844
- }
845
-
846
850
  const uploadDirect = (file, { publicKey, fileName, baseURL, secureSignature, secureExpire, store, contentType, signal, onProgress, source, integration, userAgent, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes, baseCDN, metadata }) => {
847
851
  return base(file, {
848
852
  publicKey,
@@ -878,6 +882,29 @@ const uploadDirect = (file, { publicKey, fileName, baseURL, secureSignature, sec
878
882
  .then((fileInfo) => new UploadcareFile(fileInfo, { baseCDN }));
879
883
  };
880
884
 
885
+ const uploadFromUploaded = (uuid, { publicKey, fileName, baseURL, signal, onProgress, source, integration, userAgent, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes, baseCDN }) => {
886
+ return info(uuid, {
887
+ publicKey,
888
+ baseURL,
889
+ signal,
890
+ source,
891
+ integration,
892
+ userAgent,
893
+ retryThrottledRequestMaxTimes,
894
+ retryNetworkErrorMaxTimes
895
+ })
896
+ .then((fileInfo) => new UploadcareFile(fileInfo, { baseCDN, fileName }))
897
+ .then((result) => {
898
+ // hack for node ¯\_(ツ)_/¯
899
+ if (onProgress)
900
+ onProgress({
901
+ isComputable: true,
902
+ value: 1
903
+ });
904
+ return result;
905
+ });
906
+ };
907
+
881
908
  const race = (fns, { signal } = {}) => {
882
909
  let lastError = null;
883
910
  let winnerIndex = null;
@@ -1215,35 +1242,30 @@ const uploadFromUrl = (sourceUrl, { publicKey, fileName, baseURL, baseCDN, check
1215
1242
  }))
1216
1243
  .then((fileInfo) => new UploadcareFile(fileInfo, { baseCDN }));
1217
1244
 
1218
- const uploadFromUploaded = (uuid, { publicKey, fileName, baseURL, signal, onProgress, source, integration, userAgent, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes, baseCDN }) => {
1219
- return info(uuid, {
1220
- publicKey,
1221
- baseURL,
1222
- signal,
1223
- source,
1224
- integration,
1225
- userAgent,
1226
- retryThrottledRequestMaxTimes,
1227
- retryNetworkErrorMaxTimes
1228
- })
1229
- .then((fileInfo) => new UploadcareFile(fileInfo, { baseCDN, fileName }))
1230
- .then((result) => {
1231
- // hack for node ¯\_(ツ)_/¯
1232
- if (onProgress)
1233
- onProgress({
1234
- isComputable: true,
1235
- value: 1
1236
- });
1237
- return result;
1238
- });
1245
+ const memo = new WeakMap();
1246
+ const getBlobFromReactNativeAsset = async (asset) => {
1247
+ if (memo.has(asset)) {
1248
+ return memo.get(asset);
1249
+ }
1250
+ const blob = await fetch(asset.uri).then((res) => res.blob());
1251
+ memo.set(asset, blob);
1252
+ return blob;
1239
1253
  };
1240
1254
 
1241
- /**
1242
- * Get file size.
1243
- */
1244
- const getFileSize = (file) => {
1245
- return file.length || file.size;
1255
+ const getFileSize = async (file) => {
1256
+ if (isBuffer(file)) {
1257
+ return file.length;
1258
+ }
1259
+ if (isFile(file) || isBlob(file)) {
1260
+ return file.size;
1261
+ }
1262
+ if (isReactNativeAsset(file)) {
1263
+ const blob = await getBlobFromReactNativeAsset(file);
1264
+ return blob.size;
1265
+ }
1266
+ throw new Error(`Unknown file type. Cannot determine file size.`);
1246
1267
  };
1268
+
1247
1269
  /**
1248
1270
  * Check if FileData is multipart data.
1249
1271
  */
@@ -1251,15 +1273,24 @@ const isMultipart = (fileSize, multipartMinFileSize = defaultSettings.multipartM
1251
1273
  return fileSize >= multipartMinFileSize;
1252
1274
  };
1253
1275
 
1254
- const sliceChunk = (file, index, fileSize, chunkSize) => {
1255
- const start = chunkSize * index;
1256
- const end = Math.min(start + chunkSize, fileSize);
1257
- return file.slice(start, end);
1276
+ /**
1277
+ * Uuid type guard.
1278
+ */
1279
+ const isUuid = (data) => {
1280
+ const UUID_REGEX = '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}';
1281
+ const regExp = new RegExp(UUID_REGEX);
1282
+ return !isFileData(data) && regExp.test(data);
1283
+ };
1284
+ /**
1285
+ * Url type guard.
1286
+ *
1287
+ * @param {SupportedFileInput | Url | Uuid} data
1288
+ */
1289
+ const isUrl = (data) => {
1290
+ const URL_REGEX = '^(?:\\w+:)?\\/\\/([^\\s\\.]+\\.\\S{2}|localhost[\\:?\\d]*)\\S*$';
1291
+ const regExp = new RegExp(URL_REGEX);
1292
+ return !isFileData(data) && regExp.test(data);
1258
1293
  };
1259
-
1260
- function prepareChunks(file, fileSize, chunkSize) {
1261
- return (index) => sliceChunk(file, index, fileSize, chunkSize);
1262
- }
1263
1294
 
1264
1295
  const runWithConcurrency = (concurrency, tasks) => {
1265
1296
  return new Promise((resolve, reject) => {
@@ -1296,6 +1327,16 @@ const runWithConcurrency = (concurrency, tasks) => {
1296
1327
  });
1297
1328
  };
1298
1329
 
1330
+ const sliceChunk = (file, index, fileSize, chunkSize) => {
1331
+ const start = chunkSize * index;
1332
+ const end = Math.min(start + chunkSize, fileSize);
1333
+ return file.slice(start, end);
1334
+ };
1335
+
1336
+ const prepareChunks = async (file, fileSize, chunkSize) => {
1337
+ return (index) => sliceChunk(file, index, fileSize, chunkSize);
1338
+ };
1339
+
1299
1340
  const uploadPart = (chunk, url, { publicKey, onProgress, signal, integration, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes }) => multipartUpload(chunk, url, {
1300
1341
  publicKey,
1301
1342
  onProgress,
@@ -1304,8 +1345,8 @@ const uploadPart = (chunk, url, { publicKey, onProgress, signal, integration, re
1304
1345
  retryThrottledRequestMaxTimes,
1305
1346
  retryNetworkErrorMaxTimes
1306
1347
  });
1307
- 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 }) => {
1308
- const size = fileSize || getFileSize(file);
1348
+ 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 }) => {
1349
+ const size = fileSize ?? (await getFileSize(file));
1309
1350
  let progressValues;
1310
1351
  const createProgressHandler = (totalChunks, chunkIdx) => {
1311
1352
  if (!onProgress)
@@ -1327,8 +1368,8 @@ const uploadMultipart = (file, { publicKey, fileName, fileSize, baseURL, secureS
1327
1368
  };
1328
1369
  return multipartStart(size, {
1329
1370
  publicKey,
1330
- contentType,
1331
- fileName: fileName ?? file.name,
1371
+ contentType: contentType || getContentType(file),
1372
+ fileName: fileName || getFileName(file),
1332
1373
  baseURL,
1333
1374
  secureSignature,
1334
1375
  secureExpire,
@@ -1341,8 +1382,8 @@ const uploadMultipart = (file, { publicKey, fileName, fileSize, baseURL, secureS
1341
1382
  retryNetworkErrorMaxTimes,
1342
1383
  metadata
1343
1384
  })
1344
- .then(({ uuid, parts }) => {
1345
- const getChunk = prepareChunks(file, size, multipartChunkSize);
1385
+ .then(async ({ uuid, parts }) => {
1386
+ const getChunk = await prepareChunks(file, size, multipartChunkSize);
1346
1387
  return Promise.all([
1347
1388
  uuid,
1348
1389
  runWithConcurrency(maxConcurrentRequests, parts.map((url, index) => () => uploadPart(getChunk(index), url, {
@@ -1389,14 +1430,15 @@ const uploadMultipart = (file, { publicKey, fileName, fileSize, baseURL, secureS
1389
1430
  /**
1390
1431
  * Uploads file from provided data.
1391
1432
  */
1392
- 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 }) {
1433
+ 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 }) {
1393
1434
  if (isFileData(data)) {
1394
- const fileSize = getFileSize(data);
1435
+ const fileSize = await getFileSize(data);
1395
1436
  if (isMultipart(fileSize, multipartMinFileSize)) {
1396
1437
  return uploadMultipart(data, {
1397
1438
  publicKey,
1398
1439
  contentType,
1399
1440
  multipartChunkSize,
1441
+ fileSize,
1400
1442
  fileName,
1401
1443
  baseURL,
1402
1444
  secureSignature,