@uploadcare/upload-client
Version:
Library for work with Uploadcare Upload API
1,516 lines (1,465 loc) • 59.6 kB
JavaScript
'use strict';
function isObject(o) {
return Object.prototype.toString.call(o) === '[object Object]';
}
const SEPARATOR = /\W|_/g;
function camelizeString(text) {
return text
.split(SEPARATOR)
.map((word, index) => word.charAt(0)[index > 0 ? 'toUpperCase' : 'toLowerCase']() +
word.slice(1))
.join('');
}
function camelizeArrayItems(array, { ignoreKeys } = { ignoreKeys: [] }) {
if (!Array.isArray(array)) {
return array;
}
return array.map((item) => camelizeKeys(item, { ignoreKeys }));
}
function camelizeKeys(source, { ignoreKeys } = { ignoreKeys: [] }) {
if (Array.isArray(source)) {
return camelizeArrayItems(source, { ignoreKeys });
}
if (!isObject(source)) {
return source;
}
const result = {};
for (const key of Object.keys(source)) {
let value = source[key];
if (ignoreKeys.includes(key)) {
result[key] = value;
continue;
}
if (isObject(value)) {
value = camelizeKeys(value, { ignoreKeys });
}
else if (Array.isArray(value)) {
value = camelizeArrayItems(value, { ignoreKeys });
}
result[camelizeString(key)] = value;
}
return result;
}
/**
* SetTimeout as Promise.
*
* @param {number} ms Timeout in milliseconds.
*/
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
function getUserAgent$1({ libraryName, libraryVersion, userAgent, publicKey = '', integration = '' }) {
const languageName = 'JavaScript';
if (typeof userAgent === 'string') {
return userAgent;
}
if (typeof userAgent === 'function') {
return userAgent({
publicKey,
libraryName,
libraryVersion,
languageName,
integration
});
}
const mainInfo = [libraryName, libraryVersion, publicKey]
.filter(Boolean)
.join('/');
const additionInfo = [languageName, integration].filter(Boolean).join('; ');
return `${mainInfo} (${additionInfo})`;
}
const defaultOptions = {
factor: 2,
time: 100
};
function retrier(fn, options = defaultOptions) {
let attempts = 0;
function runAttempt(fn) {
const defaultDelayTime = Math.round(options.time * options.factor ** attempts);
const retry = (ms) => delay(ms ?? defaultDelayTime).then(() => {
attempts += 1;
return runAttempt(fn);
});
return fn({
attempt: attempts,
retry
});
}
return runAttempt(fn);
}
class UploadcareError extends Error {
}
class NetworkError extends UploadcareError {
originalProgressEvent;
constructor(progressEvent) {
super();
this.name = 'NetworkError';
this.message = 'Network error';
Object.setPrototypeOf(this, NetworkError.prototype);
this.originalProgressEvent = progressEvent;
}
}
const onCancel = (signal, callback) => {
if (signal) {
if (signal.aborted) {
Promise.resolve().then(callback);
}
else {
signal.addEventListener('abort', () => callback(), { once: true });
}
}
};
class CancelError extends UploadcareError {
isCancel = true;
constructor(message = 'Request canceled') {
super(message);
this.name = 'CancelError';
Object.setPrototypeOf(this, CancelError.prototype);
}
}
const DEFAULT_INTERVAL = 500;
const poll = ({ check, interval = DEFAULT_INTERVAL, timeout, signal }) => new Promise((resolve, reject) => {
let tickTimeoutId;
let timeoutId;
onCancel(signal, () => {
tickTimeoutId && clearTimeout(tickTimeoutId);
reject(new CancelError('Poll cancelled'));
});
if (timeout) {
timeoutId = setTimeout(() => {
tickTimeoutId && clearTimeout(tickTimeoutId);
reject(new CancelError('Timed out'));
}, timeout);
}
const tick = () => {
try {
Promise.resolve(check(signal))
.then((result) => {
if (result) {
timeoutId && clearTimeout(timeoutId);
resolve(result);
}
else {
tickTimeoutId = setTimeout(tick, interval);
}
})
.catch((error) => {
timeoutId && clearTimeout(timeoutId);
reject(error);
});
}
catch (error) {
timeoutId && clearTimeout(timeoutId);
reject(error);
}
};
tickTimeoutId = setTimeout(tick, 0);
});
/*
Settings for future support:
parallelDirectUploads: 10,
*/
const defaultSettings = {
baseCDN: 'https://ucarecdn.com',
baseURL: 'https://upload.uploadcare.com',
maxContentLength: 50 * 1024 * 1024, // 50 MB
retryThrottledRequestMaxTimes: 1,
retryNetworkErrorMaxTimes: 3,
multipartMinFileSize: 25 * 1024 * 1024, // 25 MB
multipartChunkSize: 5 * 1024 * 1024, // 5 MB
multipartMinLastPartSize: 1024 * 1024, // 1MB
maxConcurrentRequests: 4,
pollingTimeoutMilliseconds: 10000,
pusherKey: '79ae88bd931ea68464d9'
};
const defaultContentType = 'application/octet-stream';
const defaultFilename = 'original';
const request = ({ method, url, data, headers = {}, signal, onProgress }) => new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const requestMethod = method?.toUpperCase() || 'GET';
let aborted = false;
/**
* Force synchronous flag to be off Some chrome versions gets
* `InvalidAccessError` error when we set `responseType` See
* https://xhr.spec.whatwg.org/#synchronous-flag and
* https://bugs.chromium.org/p/chromium/issues/detail?id=1346628
*/
xhr.open(requestMethod, url, true);
if (headers) {
Object.entries(headers).forEach((entry) => {
const [key, value] = entry;
typeof value !== 'undefined' &&
!Array.isArray(value) &&
xhr.setRequestHeader(key, value);
});
}
xhr.responseType = 'text';
onCancel(signal, () => {
aborted = true;
xhr.abort();
reject(new CancelError());
});
xhr.onload = () => {
if (xhr.status != 200) {
// analyze HTTP status of the response
reject(new Error(`Error ${xhr.status}: ${xhr.statusText}`)); // e.g. 404: Not Found
}
else {
const request = {
method: requestMethod,
url,
data,
headers: headers || undefined,
signal,
onProgress
};
// Convert the header string into an array
// of individual headers
const headersArray = xhr
.getAllResponseHeaders()
.trim()
.split(/[\r\n]+/);
// Create a map of header names to values
const responseHeaders = {};
headersArray.forEach(function (line) {
const parts = line.split(': ');
const header = parts.shift();
const value = parts.join(': ');
if (header && typeof header !== 'undefined') {
responseHeaders[header] = value;
}
});
const responseData = xhr.response;
const responseStatus = xhr.status;
resolve({
request,
data: responseData,
headers: responseHeaders,
status: responseStatus
});
}
};
xhr.onerror = (progressEvent) => {
if (aborted)
return;
// only triggers if the request couldn't be made at all
reject(new NetworkError(progressEvent));
};
if (onProgress && typeof onProgress === 'function') {
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
onProgress({
isComputable: true,
value: event.loaded / event.total
});
}
else {
onProgress({ isComputable: false });
}
};
}
if (data) {
xhr.send(data);
}
else {
xhr.send();
}
});
const isBuffer = (data) => false;
const isBlob = (data) => {
return typeof Blob !== 'undefined' && data instanceof Blob;
};
const isFile = (data) => {
return typeof File !== 'undefined' && data instanceof File;
};
const isReactNativeAsset = (data) => {
return (!!data &&
typeof data === 'object' &&
!Array.isArray(data) &&
'uri' in data &&
typeof data.uri === 'string');
};
const isFileData = (data) => {
return (isBlob(data) || isFile(data) || isBuffer() || isReactNativeAsset(data));
};
const getFileOptions = () => [];
const transformFile = (file, name, contentType) => {
if (isReactNativeAsset(file)) {
return {
uri: file.uri,
name: file.name || name,
type: file.type || contentType
};
}
if (isBlob(file)) {
const uri = URL.createObjectURL(file);
return { uri, name: name, type: file.type || contentType };
}
throw new Error(`Unsupported file type.`);
};
var getFormData = () => new FormData();
const isSimpleValue = (value) => {
return (typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'undefined');
};
const isObjectValue = (value) => {
return !!value && typeof value === 'object' && !Array.isArray(value);
};
const isFileValue = (value) => !!value &&
typeof value === 'object' &&
'data' in value &&
isFileData(value.data);
function collectParams(params, inputKey, inputValue) {
if (Array.isArray(inputValue)) {
for (const value of inputValue) {
collectParams(params, `${inputKey}[]`, value);
}
}
else if (isFileValue(inputValue)) {
const { name, contentType } = inputValue;
const file = transformFile(inputValue.data, name, contentType ?? defaultContentType);
const options = getFileOptions();
params.push([inputKey, file, ...options]);
}
else if (isObjectValue(inputValue)) {
for (const [key, value] of Object.entries(inputValue)) {
if (typeof value !== 'undefined') {
params.push([`${inputKey}[${key}]`, String(value)]);
}
}
}
else if (isSimpleValue(inputValue) && inputValue) {
params.push([inputKey, inputValue.toString()]);
}
}
function getFormDataParams(options) {
const params = [];
for (const [key, value] of Object.entries(options)) {
collectParams(params, key, value);
}
return params;
}
function buildFormData(options) {
const formData = getFormData();
const paramsList = getFormDataParams(options);
for (const params of paramsList) {
const [key, value, ...rest] = params;
// node form-data has another signature for append
formData.append(key, value, ...rest);
}
return formData;
}
class UploadError extends UploadcareError {
code;
request;
response;
headers;
constructor(message, code, request, response, headers) {
super();
this.name = 'UploadError';
this.message = message;
this.code = code;
this.request = request;
this.response = response;
this.headers = headers;
Object.setPrototypeOf(this, UploadError.prototype);
}
}
const buildSearchParams = (query) => {
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(query)) {
if (value && typeof value === 'object' && !Array.isArray(value)) {
Object.entries(value)
.filter((entry) => entry[1] ?? false)
.forEach((entry) => searchParams.set(`${key}[${entry[0]}]`, String(entry[1])));
}
else if (Array.isArray(value)) {
value.forEach((val) => {
searchParams.append(`${key}[]`, val);
});
}
else if (typeof value === 'string' && value) {
searchParams.set(key, value);
}
else if (typeof value === 'number') {
searchParams.set(key, value.toString());
}
}
return searchParams.toString();
};
const getUrl = (base, path, query) => {
const url = new URL(base);
url.pathname = (url.pathname + path).replace('//', '/');
if (query) {
url.search = buildSearchParams(query);
}
return url.toString();
};
var version = '6.14.3';
const LIBRARY_NAME = 'UploadcareUploadClient';
const LIBRARY_VERSION = version;
function getUserAgent(options) {
return getUserAgent$1({
libraryName: LIBRARY_NAME,
libraryVersion: LIBRARY_VERSION,
...options
});
}
const REQUEST_WAS_THROTTLED_CODE = 'RequestThrottledError';
const DEFAULT_RETRY_AFTER_TIMEOUT = 15000;
const DEFAULT_NETWORK_ERROR_TIMEOUT = 1000;
function getTimeoutFromThrottledRequest(error) {
const { headers } = error || {};
if (!headers || typeof headers['retry-after'] !== 'string') {
return DEFAULT_RETRY_AFTER_TIMEOUT;
}
const seconds = parseInt(headers['retry-after'], 10);
if (!Number.isFinite(seconds)) {
return DEFAULT_RETRY_AFTER_TIMEOUT;
}
return seconds * 1000;
}
function retryIfFailed(fn, options) {
const { retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes } = options;
return retrier(({ attempt, retry }) => fn().catch((error) => {
if ('response' in error &&
error?.code === REQUEST_WAS_THROTTLED_CODE &&
attempt < retryThrottledRequestMaxTimes) {
return retry(getTimeoutFromThrottledRequest(error));
}
if (error instanceof NetworkError &&
attempt < retryNetworkErrorMaxTimes) {
return retry((attempt + 1) * DEFAULT_NETWORK_ERROR_TIMEOUT);
}
throw error;
}));
}
const getContentType = (file) => {
let contentType = '';
if (isBlob(file) || isFile(file) || isReactNativeAsset(file)) {
contentType = file.type;
}
return contentType || defaultContentType;
};
const getFileName = (file) => {
let filename = '';
if (isFile(file) && file.name) {
filename = file.name;
}
else if (isBlob(file) || isBuffer()) {
filename = '';
}
else if (isReactNativeAsset(file) && file.name) {
filename = file.name;
}
return filename || defaultFilename;
};
function getStoreValue(store) {
if (typeof store === 'undefined' || store === 'auto') {
return 'auto';
}
return store ? '1' : '0';
}
/**
* Performs file uploading request to Uploadcare Upload API. Can be canceled and
* has progress.
*/
function base(file, { publicKey, fileName, contentType, baseURL = defaultSettings.baseURL, secureSignature, secureExpire, store, signal, onProgress, source = 'local', integration, userAgent, retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes = defaultSettings.retryNetworkErrorMaxTimes, metadata }) {
return retryIfFailed(() => request({
method: 'POST',
url: getUrl(baseURL, '/base/', {
jsonerrors: 1
}),
headers: {
'X-UC-User-Agent': getUserAgent({ publicKey, integration, userAgent })
},
data: buildFormData({
file: {
data: file,
name: fileName || getFileName(file),
contentType: contentType || getContentType(file)
},
UPLOADCARE_PUB_KEY: publicKey,
UPLOADCARE_STORE: getStoreValue(store),
signature: secureSignature,
expire: secureExpire,
source: source,
metadata
}),
signal,
onProgress
}).then(({ data, headers, request }) => {
const response = camelizeKeys(JSON.parse(data));
if ('error' in response) {
throw new UploadError(response.error.content, response.error.errorCode, request, response, headers);
}
else {
return response;
}
}), { retryNetworkErrorMaxTimes, retryThrottledRequestMaxTimes });
}
var TypeEnum;
(function (TypeEnum) {
TypeEnum["Token"] = "token";
TypeEnum["FileInfo"] = "file_info";
})(TypeEnum || (TypeEnum = {}));
/** Uploading files from URL. */
function fromUrl(sourceUrl, { publicKey, baseURL = defaultSettings.baseURL, store, fileName, checkForUrlDuplicates, saveUrlForRecurrentUploads, secureSignature, secureExpire, source = 'url', signal, integration, userAgent, retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes = defaultSettings.retryNetworkErrorMaxTimes, metadata }) {
return retryIfFailed(() => request({
method: 'POST',
headers: {
'X-UC-User-Agent': getUserAgent({ publicKey, integration, userAgent })
},
url: getUrl(baseURL, '/from_url/', {
jsonerrors: 1,
pub_key: publicKey,
source_url: sourceUrl,
store: getStoreValue(store),
filename: fileName,
check_URL_duplicates: checkForUrlDuplicates ? 1 : undefined,
save_URL_duplicates: saveUrlForRecurrentUploads ? 1 : undefined,
signature: secureSignature,
expire: secureExpire,
source: source,
metadata
}),
signal
}).then(({ data, headers, request }) => {
const response = camelizeKeys(JSON.parse(data));
if ('error' in response) {
throw new UploadError(response.error.content, response.error.errorCode, request, response, headers);
}
else {
return response;
}
}), { retryNetworkErrorMaxTimes, retryThrottledRequestMaxTimes });
}
var Status;
(function (Status) {
Status["Unknown"] = "unknown";
Status["Waiting"] = "waiting";
Status["Progress"] = "progress";
Status["Error"] = "error";
Status["Success"] = "success";
})(Status || (Status = {}));
const isErrorResponse = (response) => {
return 'status' in response && response.status === Status.Error;
};
/** Checking upload status and working with file tokens. */
function fromUrlStatus(token, { publicKey, baseURL = defaultSettings.baseURL, signal, integration, userAgent, retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes = defaultSettings.retryNetworkErrorMaxTimes } = {}) {
return retryIfFailed(() => request({
method: 'GET',
headers: publicKey
? {
'X-UC-User-Agent': getUserAgent({
publicKey,
integration,
userAgent
})
}
: undefined,
url: getUrl(baseURL, '/from_url/status/', {
jsonerrors: 1,
token
}),
signal
}).then(({ data, headers, request }) => {
const response = camelizeKeys(JSON.parse(data));
if ('error' in response && !isErrorResponse(response)) {
throw new UploadError(response.error.content, response.error.errorCode, request, response, headers);
}
else {
return response;
}
}), { retryNetworkErrorMaxTimes, retryThrottledRequestMaxTimes });
}
/** Create files group. */
function group(uuids, { publicKey, baseURL = defaultSettings.baseURL, jsonpCallback, secureSignature, secureExpire, signal, source, integration, userAgent, retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes = defaultSettings.retryNetworkErrorMaxTimes }) {
return retryIfFailed(() => request({
method: 'POST',
headers: {
'X-UC-User-Agent': getUserAgent({ publicKey, integration, userAgent })
},
url: getUrl(baseURL, '/group/', {
jsonerrors: 1
}),
data: buildFormData({
files: uuids,
callback: jsonpCallback,
pub_key: publicKey,
signature: secureSignature,
expire: secureExpire,
source
}),
signal
}).then(({ data, headers, request }) => {
const response = camelizeKeys(JSON.parse(data));
if ('error' in response) {
throw new UploadError(response.error.content, response.error.errorCode, request, response, headers);
}
else {
return response;
}
}), { retryNetworkErrorMaxTimes, retryThrottledRequestMaxTimes });
}
/** Get info about group. */
function groupInfo(id, { publicKey, baseURL = defaultSettings.baseURL, signal, source, integration, userAgent, retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes = defaultSettings.retryNetworkErrorMaxTimes }) {
return retryIfFailed(() => request({
method: 'GET',
headers: {
'X-UC-User-Agent': getUserAgent({ publicKey, integration, userAgent })
},
url: getUrl(baseURL, '/group/info/', {
jsonerrors: 1,
pub_key: publicKey,
group_id: id,
source
}),
signal
}).then(({ data, headers, request }) => {
const response = camelizeKeys(JSON.parse(data));
if ('error' in response) {
throw new UploadError(response.error.content, response.error.errorCode, request, response, headers);
}
else {
return response;
}
}), { retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes });
}
/** Returns a JSON dictionary holding file info. */
function info(uuid, { publicKey, baseURL = defaultSettings.baseURL, signal, source, integration, userAgent, retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes = defaultSettings.retryNetworkErrorMaxTimes }) {
return retryIfFailed(() => request({
method: 'GET',
headers: {
'X-UC-User-Agent': getUserAgent({ publicKey, integration, userAgent })
},
url: getUrl(baseURL, '/info/', {
jsonerrors: 1,
pub_key: publicKey,
file_id: uuid,
source
}),
signal
}).then(({ data, headers, request }) => {
const response = camelizeKeys(JSON.parse(data));
if ('error' in response) {
throw new UploadError(response.error.content, response.error.errorCode, request, response, headers);
}
else {
return response;
}
}), { retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes });
}
/** Start multipart uploading. */
function multipartStart(size, { publicKey, contentType, fileName, multipartChunkSize = defaultSettings.multipartChunkSize, baseURL = '', secureSignature, secureExpire, store, signal, source = 'local', integration, userAgent, retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes = defaultSettings.retryNetworkErrorMaxTimes, metadata }) {
return retryIfFailed(() => request({
method: 'POST',
url: getUrl(baseURL, '/multipart/start/', { jsonerrors: 1 }),
headers: {
'X-UC-User-Agent': getUserAgent({ publicKey, integration, userAgent })
},
data: buildFormData({
filename: fileName || defaultFilename,
size: size,
content_type: contentType || defaultContentType,
part_size: multipartChunkSize,
UPLOADCARE_STORE: getStoreValue(store),
UPLOADCARE_PUB_KEY: publicKey,
signature: secureSignature,
expire: secureExpire,
source: source,
metadata
}),
signal
}).then(({ data, headers, request }) => {
const response = camelizeKeys(JSON.parse(data));
if ('error' in response) {
throw new UploadError(response.error.content, response.error.errorCode, request, response, headers);
}
else {
// convert to array
response.parts = Object.keys(response.parts).map((key) => response.parts[Number(key)]);
return response;
}
}), { retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes });
}
/** Complete multipart uploading. */
function multipartUpload(part, url, { contentType, signal, onProgress, retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes = defaultSettings.retryNetworkErrorMaxTimes }) {
return retryIfFailed(() => request({
method: 'PUT',
url,
data: part,
// Upload request can't be non-computable because we always know exact size
onProgress: onProgress,
signal,
headers: {
'Content-Type': contentType || defaultContentType
}
})
.then((result) => {
// hack for node ¯\_(ツ)_/¯
if (onProgress)
onProgress({
isComputable: true,
value: 1
});
return result;
})
.then(({ status }) => ({ code: status })), {
retryThrottledRequestMaxTimes,
retryNetworkErrorMaxTimes
});
}
/** Complete multipart uploading. */
function multipartComplete(uuid, { publicKey, baseURL = defaultSettings.baseURL, source = 'local', signal, integration, userAgent, retryThrottledRequestMaxTimes = defaultSettings.retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes = defaultSettings.retryNetworkErrorMaxTimes }) {
return retryIfFailed(() => request({
method: 'POST',
url: getUrl(baseURL, '/multipart/complete/', { jsonerrors: 1 }),
headers: {
'X-UC-User-Agent': getUserAgent({ publicKey, integration, userAgent })
},
data: buildFormData({
uuid: uuid,
UPLOADCARE_PUB_KEY: publicKey,
source: source
}),
signal
}).then(({ data, headers, request }) => {
const response = camelizeKeys(JSON.parse(data));
if ('error' in response) {
throw new UploadError(response.error.content, response.error.errorCode, request, response, headers);
}
else {
return response;
}
}), { retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes });
}
function isReadyPoll(uuid, { publicKey, baseURL, source, integration, userAgent, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes, signal, onProgress }) {
return poll({
check: (signal) => info(uuid, {
publicKey,
baseURL,
signal,
source,
integration,
userAgent,
retryThrottledRequestMaxTimes,
retryNetworkErrorMaxTimes
}).then((response) => {
if (response.isReady) {
return response;
}
onProgress && onProgress({ isComputable: true, value: 1 });
return false;
}),
signal
});
}
function isGroupFileInfo(fileInfo) {
return 'defaultEffects' in fileInfo;
}
class UploadcareFile {
uuid;
name;
size;
isStored;
isImage;
mimeType;
cdnUrl;
s3Url;
originalFilename;
imageInfo;
videoInfo;
contentInfo;
metadata;
s3Bucket;
defaultEffects = null;
constructor(fileInfo, { baseCDN = defaultSettings.baseCDN, fileName } = {}) {
const { uuid, s3Bucket } = fileInfo;
const cdnUrl = getUrl(baseCDN, `${uuid}/`);
const s3Url = s3Bucket
? getUrl(`https://${s3Bucket}.s3.amazonaws.com/`, `${uuid}/${fileInfo.filename}`)
: null;
this.uuid = uuid;
this.name = fileName || fileInfo.filename;
this.size = fileInfo.size;
this.isStored = fileInfo.isStored;
this.isImage = fileInfo.isImage;
this.mimeType = fileInfo.mimeType;
this.cdnUrl = cdnUrl;
this.originalFilename = fileInfo.originalFilename;
this.imageInfo = fileInfo.imageInfo;
this.videoInfo = fileInfo.videoInfo;
this.contentInfo = fileInfo.contentInfo;
this.metadata = fileInfo.metadata || null;
this.s3Bucket = s3Bucket || null;
this.s3Url = s3Url;
if (isGroupFileInfo(fileInfo)) {
this.defaultEffects = fileInfo.defaultEffects;
}
}
}
const uploadDirect = (file, { publicKey, fileName, baseURL, secureSignature, secureExpire, store, contentType, signal, onProgress, source, integration, userAgent, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes, baseCDN, metadata }) => {
return base(file, {
publicKey,
fileName,
contentType,
baseURL,
secureSignature,
secureExpire,
store,
signal,
onProgress,
source,
integration,
userAgent,
retryThrottledRequestMaxTimes,
retryNetworkErrorMaxTimes,
metadata
})
.then(({ file }) => {
return isReadyPoll(file, {
publicKey,
baseURL,
source,
integration,
userAgent,
retryThrottledRequestMaxTimes,
retryNetworkErrorMaxTimes,
onProgress,
signal
});
})
.then((fileInfo) => new UploadcareFile(fileInfo, { baseCDN }));
};
const uploadFromUploaded = (uuid, { publicKey, fileName, baseURL, signal, onProgress, source, integration, userAgent, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes, baseCDN }) => {
return info(uuid, {
publicKey,
baseURL,
signal,
source,
integration,
userAgent,
retryThrottledRequestMaxTimes,
retryNetworkErrorMaxTimes
})
.then((fileInfo) => new UploadcareFile(fileInfo, { baseCDN, fileName }))
.then((result) => {
// hack for node ¯\_(ツ)_/¯
if (onProgress)
onProgress({
isComputable: true,
value: 1
});
return result;
});
};
const race = (fns, { signal } = {}) => {
let lastError = null;
let winnerIndex = null;
const controllers = fns.map(() => new AbortController());
const createStopRaceCallback = (i) => () => {
winnerIndex = i;
controllers.forEach((controller, index) => index !== i && controller.abort());
};
onCancel(signal, () => {
controllers.forEach((controller) => controller.abort());
});
return Promise.all(fns.map((fn, i) => {
const stopRace = createStopRaceCallback(i);
return Promise.resolve()
.then(() => fn({ stopRace, signal: controllers[i].signal }))
.then((result) => {
stopRace();
return result;
})
.catch((error) => {
lastError = error;
return null;
});
})).then((results) => {
if (winnerIndex === null) {
throw lastError;
}
else {
return results[winnerIndex];
}
});
};
var WebSocket = window.WebSocket;
class Events {
events = Object.create({});
emit(event, data) {
this.events[event]?.forEach((fn) => fn(data));
}
on(event, callback) {
this.events[event] = this.events[event] || [];
this.events[event].push(callback);
}
off(event, callback) {
if (callback) {
this.events[event] = this.events[event].filter((fn) => fn !== callback);
}
else {
this.events[event] = [];
}
}
}
const response = (type, data) => {
if (type === 'success') {
return { status: Status.Success, ...data };
}
if (type === 'progress') {
return { status: Status.Progress, ...data };
}
return { status: Status.Error, ...data };
};
class Pusher {
key;
disconnectTime;
ws = undefined;
queue = [];
isConnected = false;
subscribers = 0;
emmitter = new Events();
disconnectTimeoutId = null;
constructor(pusherKey, disconnectTime = 30000) {
this.key = pusherKey;
this.disconnectTime = disconnectTime;
}
connect() {
this.disconnectTimeoutId && clearTimeout(this.disconnectTimeoutId);
if (!this.isConnected && !this.ws) {
const pusherUrl = `wss://ws.pusherapp.com/app/${this.key}?protocol=5&client=js&version=1.12.2`;
this.ws = new WebSocket(pusherUrl);
this.ws.addEventListener('error', (error) => {
this.emmitter.emit('error', new Error(error.message));
});
this.emmitter.on('connected', () => {
this.isConnected = true;
this.queue.forEach((message) => this.send(message.event, message.data));
this.queue = [];
});
this.ws.addEventListener('message', (e) => {
const data = JSON.parse(e.data.toString());
switch (data.event) {
case 'pusher:connection_established': {
this.emmitter.emit('connected', undefined);
break;
}
case 'pusher:ping': {
this.send('pusher:pong', {});
break;
}
case 'progress':
case 'success':
case 'fail': {
this.emmitter.emit(data.channel, response(data.event, JSON.parse(data.data)));
}
}
});
}
}
disconnect() {
const actualDisconect = () => {
this.ws?.close();
this.ws = undefined;
this.isConnected = false;
};
if (this.disconnectTime) {
this.disconnectTimeoutId = setTimeout(() => {
actualDisconect();
}, this.disconnectTime);
}
else {
actualDisconect();
}
}
send(event, data) {
const str = JSON.stringify({ event, data });
this.ws?.send(str);
}
subscribe(token, handler) {
this.subscribers += 1;
this.connect();
const channel = `task-status-${token}`;
const message = {
event: 'pusher:subscribe',
data: { channel }
};
this.emmitter.on(channel, handler);
if (this.isConnected) {
this.send(message.event, message.data);
}
else {
this.queue.push(message);
}
}
unsubscribe(token) {
this.subscribers -= 1;
const channel = `task-status-${token}`;
const message = {
event: 'pusher:unsubscribe',
data: { channel }
};
this.emmitter.off(channel);
if (this.isConnected) {
this.send(message.event, message.data);
}
else {
this.queue = this.queue.filter((msg) => msg.data.channel !== channel);
}
if (this.subscribers === 0) {
this.disconnect();
}
}
onError(callback) {
this.emmitter.on('error', callback);
return () => this.emmitter.off('error', callback);
}
}
let pusher = null;
const getPusher = (key) => {
if (!pusher) {
// no timeout for nodeJS and 30000 ms for browser
const disconectTimeout = typeof window === 'undefined' ? 0 : 30000;
pusher = new Pusher(key, disconectTimeout);
}
return pusher;
};
const preconnect = (key) => {
getPusher(key).connect();
};
function pollStrategy({ token, publicKey, baseURL, integration, userAgent, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes, onProgress, signal }) {
return poll({
check: (signal) => fromUrlStatus(token, {
publicKey,
baseURL,
integration,
userAgent,
retryThrottledRequestMaxTimes,
retryNetworkErrorMaxTimes,
signal
}).then((response) => {
switch (response.status) {
case Status.Error: {
return new UploadError(response.error, response.errorCode);
}
case Status.Waiting: {
return false;
}
case Status.Unknown: {
return new UploadError(`Token "${token}" was not found.`);
}
case Status.Progress: {
if (onProgress) {
if (response.total === 'unknown') {
onProgress({ isComputable: false });
}
else {
onProgress({
isComputable: true,
value: response.done / response.total
});
}
}
return false;
}
case Status.Success: {
if (onProgress)
onProgress({
isComputable: true,
value: response.done / response.total
});
return response;
}
default: {
throw new Error('Unknown status');
}
}
}),
signal
});
}
const pushStrategy = ({ token, pusherKey, signal, onProgress }) => new Promise((resolve, reject) => {
const pusher = getPusher(pusherKey);
const unsubErrorHandler = pusher.onError(reject);
const destroy = () => {
unsubErrorHandler();
pusher.unsubscribe(token);
};
onCancel(signal, () => {
destroy();
reject(new CancelError('pusher cancelled'));
});
pusher.subscribe(token, (result) => {
switch (result.status) {
case Status.Progress: {
if (onProgress) {
if (result.total === 'unknown') {
onProgress({ isComputable: false });
}
else {
onProgress({
isComputable: true,
value: result.done / result.total
});
}
}
break;
}
case Status.Success: {
destroy();
if (onProgress)
onProgress({
isComputable: true,
value: result.done / result.total
});
resolve(result);
break;
}
case Status.Error: {
destroy();
reject(new UploadError(result.msg, result.error_code));
}
}
});
});
const uploadFromUrl = (sourceUrl, { publicKey, fileName, baseURL, baseCDN, checkForUrlDuplicates, saveUrlForRecurrentUploads, secureSignature, secureExpire, store, signal, onProgress, source, integration, userAgent, retryThrottledRequestMaxTimes, pusherKey = defaultSettings.pusherKey, metadata }) => Promise.resolve(preconnect(pusherKey))
.then(() => fromUrl(sourceUrl, {
publicKey,
fileName,
baseURL,
checkForUrlDuplicates,
saveUrlForRecurrentUploads,
secureSignature,
secureExpire,
store,
signal,
source,
integration,
userAgent,
retryThrottledRequestMaxTimes,
metadata
}))
.catch((error) => {
const pusher = getPusher(pusherKey);
pusher?.disconnect();
return Promise.reject(error);
})
.then((urlResponse) => {
if (urlResponse.type === TypeEnum.FileInfo) {
return urlResponse;
}
else {
return race([
({ signal }) => pollStrategy({
token: urlResponse.token,
publicKey,
baseURL,
integration,
userAgent,
retryThrottledRequestMaxTimes,
onProgress,
signal
}),
({ signal }) => pushStrategy({
token: urlResponse.token,
pusherKey,
signal,
onProgress
})
], { signal });
}
})
.then((result) => {
if (result instanceof UploadError)
throw result;
return result;
})
.then((result) => isReadyPoll(result.uuid, {
publicKey,
baseURL,
integration,
userAgent,
retryThrottledRequestMaxTimes,
onProgress,
signal
}))
.then((fileInfo) => new UploadcareFile(fileInfo, { baseCDN }));
const memo = new WeakMap();
const getBlobFromReactNativeAsset = async (asset) => {
if (memo.has(asset)) {
return memo.get(asset);
}
const blob = await fetch(asset.uri).then((res) => res.blob());
memo.set(asset, blob);
return blob;
};
const getFileSize = async (file) => {
if (isFile(file) || isBlob(file)) {
return file.size;
}
if (isReactNativeAsset(file)) {
const blob = await getBlobFromReactNativeAsset(file);
return blob.size;
}
throw new Error(`Unknown file type. Cannot determine file size.`);
};
/** Check if FileData is multipart data. */
const isMultipart = (fileSize, multipartMinFileSize = defaultSettings.multipartMinFileSize) => {
return fileSize >= multipartMinFileSize;
};
/** Uuid type guard. */
const isUuid = (data) => {
const UUID_REGEX = '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}';
const regExp = new RegExp(UUID_REGEX);
return !isFileData(data) && regExp.test(data);
};
/**
* Url type guard.
*
* @param {SupportedFileInput | Url | Uuid} data
*/
const isUrl = (data) => {
const URL_REGEX = '^(?:\\w+:)?\\/\\/([^\\s\\.]+\\.\\S{2}|localhost[\\:?\\d]*)\\S*$';
const regExp = new RegExp(URL_REGEX);
return !isFileData(data) && regExp.test(data);
};
const runWithConcurrency = (concurrency, tasks) => {
return new Promise((resolve, reject) => {
const results = [];
let rejected = false;
let settled = tasks.length;
const forRun = [...tasks];
const run = () => {
const index = tasks.length - forRun.length;
const next = forRun.shift();
if (next) {
next()
.then((result) => {
if (rejected)
return;
results[index] = result;
settled -= 1;
if (settled) {
run();
}
else {
resolve(results);
}
})
.catch((error) => {
rejected = true;
reject(error);
});
}
};
for (let i = 0; i < concurrency; i++) {
run();
}
});
};
const sliceChunk = (file, index, fileSize, chunkSize) => {
const start = chunkSize * index;
const end = Math.min(start + chunkSize, fileSize);
return file.slice(start, end);
};
/**
* React-native hack for blob slicing
*
* We need to store references to sliced blobs to prevent source blob from being
* deallocated until uploading complete. Access to deallocated blob causes app
* crash.
*
* See https://github.com/uploadcare/uploadcare-js-api-clients/issues/306 and
* https://github.com/facebook/react-native/issues/27543
*/
const prepareChunks = async (file, fileSize, chunkSize) => {
let blob;
if (isReactNativeAsset(file)) {
blob = await getBlobFromReactNativeAsset(file);
}
else {
blob = file;
}
return (index) => {
const chunk = sliceChunk(blob, index, fileSize, chunkSize);
return chunk;
};
};
const uploadPart = (chunk, url, { publicKey, contentType, onProgress, signal, integration, retryThrottledRequestMaxTimes, retryNetworkErrorMaxTimes }) => multipartUpload(chunk, url, {
publicKey,
contentType,
onProgress,
signal,
integration,
retryThrottledRequestMaxTimes,
retryNetworkErrorMaxTimes
});
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 }) => {
const size = fileSize ?? (await getFileSize(file));
let progressValues;
const createProgressHandler = (totalChunks, chunkIdx) => {
if (!onProgress)
return;
if (!progressValues) {
progressValues = Array(totalChunks).fill(0);
}
const sum = (values) => values.reduce((sum, next) => sum + next, 0);
return (info) => {
if (!info.isComputable) {
return;
}
progressValues[chunkIdx] = info.value;
onProgress({
isComputable: true,
value: sum(progressValues) / totalChunks
});
};
};
contentType ||= getContentType(file);
return multipartStart(size, {
publicKey,
contentType,
fileName: fileName || getFileName(file),
baseURL,
secureSignature,
secureExpire,
store,
signal,
source,
integration,
userAgent,
retryThrottledRequestMaxTimes,
retryNetworkErrorMaxTimes,
metadata,
multipartChunkSize
})
.then(async ({ uuid, parts }) => {
const getChunk = await prepareChunks(file, size, multipartChunkSize);
return Promise.all([
uuid,
runWithConcurrency(maxConcurrentRequests, parts.map((url, index) => () => uploadPart(getChunk(index), url, {
publicKey,
contentType,
onProgress: createProgressHandler(parts.length, index),
signal,
integration,
retryThrottledRequestMaxTimes,
retryNetworkErrorMaxTimes
})))
]);
})
.then(([uuid]) => multipartComplete(uuid, {
publicKey,
baseURL,
source,
integration,
userAgent,
retryThrottledRequestMaxTimes,
retryNetworkErrorMaxTimes
}))
.then((fileInfo) => {
if (fileInfo.isReady) {
return fileInfo;
}
else {
return isReadyPoll(fileInfo.uuid, {
publicKey,
baseURL,
source,
integration,
userAgent,
retryThrottledRequestMaxTimes,
retryNetworkErrorMaxTimes,
onProgress,
signal
});
}
})
.then((fileInfo) => new UploadcareFile(fileInfo, { baseCDN }));
};
/** Uploads file from provided data. */
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 }) {
if (isFileData(data)) {
const fileSize = await getFileSize(data);
if (isMultipart(fileSize, multipartMinFileSize)) {
return uploadMultipart(data, {
publicKey,
contentType,
multipartChunkSize,
fileSize,
fileName,
baseURL,
secureSignature,
secureExpire,
store,
signal,
onProgress,
source,
integration,
userAgent,
maxConcurrentRequests,
retryThrottledRequestMaxTimes,
retryNetworkErrorMaxTimes,
baseCDN,
metadata
});
}
return uploadDirect(data, {
publicKey,
fileName,
contentType,
baseURL,
secureSignature,
secureExpire,
store,
signal,
onProgress,
source,
integration,
userAgent,
retryThrottledRequestMaxTimes,
retryNetworkErrorMaxTimes,
baseCDN,
metadata
});
}
if (isUrl(data)) {
return uploadFromUrl(data, {
publicKey,
fileName,
baseURL,
baseCDN,
checkForUrlDuplicates,
saveUrlForRecurrentUploads,
secureSignature,
secureExpire,
store,
signal,
onProgress,
source,
integration,
userAgent,
retryThrottledRequestMaxTimes,
retryNetworkErrorMaxTimes,
pusherKey,
metadata
});
}
if (isUuid(data)) {
return uploadFromUploaded(data, {
publicKey,
fileName,
baseURL,
signal,
onProgress,
source,
integration,
userAgent,
retryThrottledRequestMaxTimes,
retryNetworkErrorMaxTimes,
baseCDN
});
}
throw new Type