@uploadx/client
Version:
Resumable upload client for browser and Node.js
682 lines (681 loc) • 34.2 kB
JavaScript
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UploadxClient = exports.AbortError = void 0;
var axios_1 = __importDefault(require("axios"));
var axios_retry_1 = __importStar(require("axios-retry"));
var isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
var AbortError = /** @class */ (function (_super) {
__extends(AbortError, _super);
function AbortError(message) {
if (message === void 0) { message = 'Operation aborted'; }
var _this = _super.call(this, message) || this;
_this.name = 'AbortError';
return _this;
}
return AbortError;
}(Error));
exports.AbortError = AbortError;
/**
* Client for handling resumable file uploads
*
* Supports chunked uploads with progress tracking, resuming interrupted uploads,
* and works in both browser and Node.js environments.
*/
var UploadxClient = /** @class */ (function () {
/**
* Creates a new UploadxClient instance
* @param config - Configuration options for chunk size, retry behavior, and axios settings
*/
function UploadxClient(config) {
if (config === void 0) { config = {}; }
this.abortController = new AbortController();
this.chunkSize = config.chunkSize || 5 * 1024 * 1024;
this.client = axios_1.default.create(__assign({ maxBodyLength: Number.POSITIVE_INFINITY, maxContentLength: Number.POSITIVE_INFINITY, timeout: 60000, validateStatus: function (status) { return (status >= 200 && status < 400) || status === 308; }, signal: this.abortController.signal }, config.requestConfig));
(0, axios_retry_1.default)(this.client, __assign({ retries: 5, retryDelay: axios_retry_1.exponentialDelay, retryCondition: function (error) {
var _a, _b;
var isNetworkError = axios_retry_1.default.isNetworkError(error) || error.code === 'ECONNRESET';
return (isNetworkError || axios_retry_1.default.isRetryableError(error) || ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 429 || (((_b = error.response) === null || _b === void 0 ? void 0 : _b.status) || 0) >= 500);
} }, config.retryConfig));
}
/**
* Aborts all ongoing upload operations
* Cancels any in-progress uploads initiated by this client instance
*/
UploadxClient.prototype.abort = function () {
this.abortController.abort();
// Create a new controller for future operations
this.abortController = new AbortController();
};
/**
* Deletes an existing upload from the server
*/
UploadxClient.prototype.deleteUpload = function (url, signal) {
return __awaiter(this, void 0, void 0, function () {
var error_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
_a.trys.push([0, 2, , 3]);
return [4 /*yield*/, this.client.delete(url, { signal: signal })];
case 1:
_a.sent();
return [3 /*break*/, 3];
case 2:
error_1 = _a.sent();
throw this.handleError(error_1, 'Failed to delete upload');
case 3: return [2 /*return*/];
}
});
});
};
/**
*
* Updates the metadata of an existing upload
*/
UploadxClient.prototype.updateUpload = function (url, metadata, signal) {
return __awaiter(this, void 0, void 0, function () {
var error_2;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
_a.trys.push([0, 2, , 3]);
return [4 /*yield*/, this.client.patch(url, metadata, {
headers: {
'Content-Type': 'application/json',
},
signal: signal,
})];
case 1:
_a.sent();
return [3 /*break*/, 3];
case 2:
error_2 = _a.sent();
throw this.handleError(error_2, 'Failed to update metadata');
case 3: return [2 /*return*/];
}
});
});
};
/**
* Creates a new upload session on the server
*/
UploadxClient.prototype.createUpload = function (endpoint, metadata, signal) {
return __awaiter(this, void 0, void 0, function () {
var response, rangeHeader, error_3;
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
_b.trys.push([0, 2, , 3]);
return [4 /*yield*/, this.client.post(endpoint, metadata, {
headers: {
'X-Upload-Content-Length': metadata.size,
'X-Upload-Content-Type': metadata.mimeType || 'application/octet-stream',
'Content-Type': 'application/json',
},
signal: signal,
})];
case 1:
response = _b.sent();
if (!response.headers.location) {
throw new Error('Missing Location header in response');
}
rangeHeader = (_a = response === null || response === void 0 ? void 0 : response.headers) === null || _a === void 0 ? void 0 : _a.range;
return [2 /*return*/, {
url: new URL(response.headers.location, endpoint).toString(),
uploadedBytes: this.parseRangeHeader(rangeHeader),
}];
case 2:
error_3 = _b.sent();
throw this.handleError(error_3, 'Session creation failed');
case 3: return [2 /*return*/];
}
});
});
};
/**
* Creates an upload session for a file in Node.js environment
*/
UploadxClient.prototype.createFileUpload = function (filePath, metadata, endpoint, signal) {
return __awaiter(this, void 0, void 0, function () {
var fsPromises, stats, fileSize, actualMetadata;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!isNode) {
throw new Error('uploadAsFile is only available in Node.js environment');
}
return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(require('node:fs/promises')); })];
case 1:
fsPromises = _a.sent();
return [4 /*yield*/, fsPromises.stat(filePath)];
case 2:
stats = _a.sent();
fileSize = stats.size;
actualMetadata = __assign(__assign({}, metadata), { size: fileSize, lastModified: metadata.lastModified || stats.mtimeMs });
return [2 /*return*/, this.createUpload(endpoint, actualMetadata, signal)];
}
});
});
};
/**
* Uploads a file from disk (Node.js only)
*/
UploadxClient.prototype.fileUpload = function (endpoint, filePath, metadata, onProgress, signal) {
return __awaiter(this, void 0, void 0, function () {
var session, sessionUrl, start, totalSize, error_4;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
_a.trys.push([0, 3, , 4]);
return [4 /*yield*/, this.createFileUpload(filePath, metadata, endpoint, signal)];
case 1:
session = _a.sent();
sessionUrl = session.url;
start = session.uploadedBytes || 0;
totalSize = metadata.size;
return [4 /*yield*/, this.uploadFileInChunks(sessionUrl, filePath, start, totalSize, onProgress, signal)];
case 2:
_a.sent();
return [3 /*break*/, 4];
case 3:
error_4 = _a.sent();
// Don't throw if the operation was aborted
if (error_4 instanceof AbortError) {
return [2 /*return*/];
}
throw this.handleError(error_4, "File upload failed for ".concat(filePath));
case 4: return [2 /*return*/];
}
});
});
};
/**
* Uploads data from various sources (Blob, File, Stream, Buffer)
*/
UploadxClient.prototype.upload = function (endpoint, data, metadata, onProgress, signal) {
return __awaiter(this, void 0, void 0, function () {
var _a, url, uploadedBytes, start, totalSize, error_5;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
_b.trys.push([0, 3, , 4]);
return [4 /*yield*/, this.createUpload(endpoint, metadata, signal)];
case 1:
_a = _b.sent(), url = _a.url, uploadedBytes = _a.uploadedBytes;
start = uploadedBytes || 0;
totalSize = metadata.size;
return [4 /*yield*/, this.uploadDataInChunks(data, url, start, totalSize, onProgress, signal)];
case 2:
_b.sent();
return [3 /*break*/, 4];
case 3:
error_5 = _b.sent();
// Don't throw if the operation was aborted
if (error_5 instanceof AbortError) {
return [2 /*return*/];
}
throw this.handleError(error_5, 'Upload failed');
case 4: return [2 /*return*/];
}
});
});
};
/**
* Resumes an upload from a previously created session
*/
UploadxClient.prototype.resumeUpload = function (url, data, metadata, onProgress, signal) {
return __awaiter(this, void 0, void 0, function () {
var response, start, totalSize, error_6;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
_a.trys.push([0, 3, , 4]);
return [4 /*yield*/, this.getUploadStatus(url, metadata, signal)];
case 1:
response = _a.sent();
start = response.uploadedBytes;
totalSize = metadata.size;
return [4 /*yield*/, this.uploadDataInChunks(data, url, start, totalSize, onProgress, signal)];
case 2:
_a.sent();
return [3 /*break*/, 4];
case 3:
error_6 = _a.sent();
// Don't throw if the operation was aborted
if (error_6 instanceof AbortError) {
return [2 /*return*/];
}
throw this.handleError(error_6, 'Upload failed');
case 4: return [2 /*return*/];
}
});
});
};
/**
* Resumes a file upload from disk (Node.js only)
*/
UploadxClient.prototype.resumeFileUpload = function (url, filePath, metadata, onProgress, signal) {
return __awaiter(this, void 0, void 0, function () {
var response, start, totalSize, error_7;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!isNode) {
throw new Error('uploadAsFile is only available in Node.js environment');
}
_a.label = 1;
case 1:
_a.trys.push([1, 4, , 5]);
return [4 /*yield*/, this.getUploadStatus(url, metadata, signal)];
case 2:
response = _a.sent();
start = response.uploadedBytes;
totalSize = metadata.size;
return [4 /*yield*/, this.uploadFileInChunks(url, filePath, start, totalSize, onProgress, signal)];
case 3:
_a.sent();
return [3 /*break*/, 5];
case 4:
error_7 = _a.sent();
// Don't throw if the operation was aborted
if (error_7 instanceof AbortError) {
return [2 /*return*/];
}
throw this.handleError(error_7, "File upload failed for ".concat(filePath));
case 5: return [2 /*return*/];
}
});
});
};
/**
* Gets the current upload progress from the server
*/
UploadxClient.prototype.getUploadStatus = function (url, metadata, signal) {
return __awaiter(this, void 0, void 0, function () {
var headers, response, rangeHeader, uploadedBytes;
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
headers = {
'Content-Type': 'application/octet-stream',
'Content-Range': "bytes */".concat((metadata === null || metadata === void 0 ? void 0 : metadata.size) || '*'),
};
return [4 /*yield*/, this.client.put(url, null, {
headers: headers,
signal: signal,
})];
case 1:
response = _b.sent();
rangeHeader = (_a = response === null || response === void 0 ? void 0 : response.headers) === null || _a === void 0 ? void 0 : _a.range;
uploadedBytes = this.parseRangeHeader(rangeHeader);
return [2 /*return*/, { uploadedBytes: uploadedBytes }];
}
});
});
};
UploadxClient.prototype.uploadChunk = function (url, data, start, end, totalSize, onProgress, signal) {
return __awaiter(this, void 0, void 0, function () {
var headers, response, rangeHeader, uploadedBytes;
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
headers = {
'Content-Type': 'application/octet-stream',
'Content-Range': "bytes ".concat(start, "-").concat(end - 1, "/").concat(totalSize),
};
return [4 /*yield*/, this.client.put(url, data, {
headers: headers,
signal: signal,
onUploadProgress: onProgress
? function (progressEvent) {
var chunkUploaded = progressEvent.loaded;
var totalUploaded = start + chunkUploaded;
var progress = totalUploaded / totalSize;
onProgress(Math.min(progress, 1));
}
: undefined,
})];
case 1:
response = _b.sent();
rangeHeader = (_a = response === null || response === void 0 ? void 0 : response.headers) === null || _a === void 0 ? void 0 : _a.range;
uploadedBytes = this.parseRangeHeader(rangeHeader);
return [2 /*return*/, { uploadedBytes: uploadedBytes }];
}
});
});
};
UploadxClient.prototype.uploadDataInChunks = function (data, url, start, totalSize, onProgress, signal) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!this.isBlob(data)) return [3 /*break*/, 2];
return [4 /*yield*/, this.uploadBlobInChunks(url, data, start, totalSize, onProgress, signal)];
case 1:
_a.sent();
return [3 /*break*/, 6];
case 2:
if (!this.isStream(data)) return [3 /*break*/, 4];
return [4 /*yield*/, this.uploadStreamInChunks(url, data, start, totalSize, onProgress, signal)];
case 3:
_a.sent();
return [3 /*break*/, 6];
case 4: return [4 /*yield*/, this.uploadBufferInChunks(url, data, start, totalSize, onProgress, signal)];
case 5:
_a.sent();
_a.label = 6;
case 6: return [2 /*return*/];
}
});
});
};
UploadxClient.prototype.uploadFileInChunks = function (url, filePath, start, totalSize, onProgress, signal) {
return __awaiter(this, void 0, void 0, function () {
var createReadStream, position, end, fileChunk, result, newPosition;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!isNode) {
throw new Error('uploadAsFile is only available in Node.js environment');
}
return [4 /*yield*/, Promise.resolve().then(function () { return __importStar(require('node:fs')); })];
case 1:
createReadStream = (_a.sent()).createReadStream;
position = start;
_a.label = 2;
case 2:
if (!(position < totalSize)) return [3 /*break*/, 4];
// Check if the operation was aborted
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
throw new AbortError();
}
end = Math.min(position + this.chunkSize, totalSize);
fileChunk = createReadStream(filePath, {
start: position,
end: end,
});
return [4 /*yield*/, this.uploadChunk(url, fileChunk, position, end, totalSize, onProgress, signal)];
case 3:
result = _a.sent();
position = end;
if (result.uploadedBytes !== undefined) {
newPosition = result.uploadedBytes;
if (newPosition > position) {
position = newPosition;
}
}
return [3 /*break*/, 2];
case 4: return [2 /*return*/];
}
});
});
};
UploadxClient.prototype.uploadBlobInChunks = function (url, blob, start, totalSize, onProgress, signal) {
return __awaiter(this, void 0, void 0, function () {
var position, end, chunk;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
position = start;
_a.label = 1;
case 1:
if (!(position < totalSize)) return [3 /*break*/, 3];
// Check if the operation was aborted
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
throw new AbortError();
}
end = Math.min(position + this.chunkSize, totalSize);
chunk = blob.slice(position, end);
return [4 /*yield*/, this.uploadChunk(url, chunk, position, end, totalSize, onProgress, signal)];
case 2:
_a.sent();
position = end;
return [3 /*break*/, 1];
case 3: return [2 /*return*/];
}
});
});
};
UploadxClient.prototype.uploadStreamInChunks = function (url, stream, start, totalSize, onProgress, signal) {
return __awaiter(this, void 0, void 0, function () {
var position, chunks, chunksSize;
var _this = this;
return __generator(this, function (_a) {
if (!isNode) {
throw new Error('Stream uploads are only available in Node.js environment');
}
position = start;
chunks = [];
chunksSize = 0;
return [2 /*return*/, new Promise(function (resolve, reject) {
var abortHandler = function () {
reject(new AbortError());
};
if (signal) {
if (signal.aborted) {
abortHandler();
return;
}
signal.addEventListener('abort', abortHandler);
}
stream.on('data', function (chunk) {
chunks.push(chunk);
chunksSize += chunk.length;
var _loop_1 = function () {
var end = Math.min(position + _this.chunkSize, totalSize);
var chunkBuffer = Buffer.concat(chunks);
chunks = [chunkBuffer.slice(end - position)];
chunksSize = chunkBuffer.length - (end - position);
stream.pause();
_this.uploadChunk(url, chunkBuffer.slice(0, end - position), position, end, totalSize, onProgress, signal)
.then(function () {
position = end;
stream.resume();
})
.catch(function (error) {
reject(error);
});
};
while (chunksSize >= _this.chunkSize) {
_loop_1();
}
});
stream.on('end', function () { return __awaiter(_this, void 0, void 0, function () {
var end, chunkBuffer, error_8;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (signal) {
signal.removeEventListener('abort', abortHandler);
}
if (!(chunks.length > 0 && position < totalSize)) return [3 /*break*/, 4];
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
reject(new AbortError());
return [2 /*return*/];
}
end = Math.min(position + chunksSize, totalSize);
chunkBuffer = Buffer.concat(chunks);
_a.label = 1;
case 1:
_a.trys.push([1, 3, , 4]);
return [4 /*yield*/, this.uploadChunk(url, chunkBuffer, position, end, totalSize, onProgress, signal)];
case 2:
_a.sent();
position = end;
return [3 /*break*/, 4];
case 3:
error_8 = _a.sent();
reject(error_8);
return [2 /*return*/];
case 4:
resolve();
return [2 /*return*/];
}
});
}); });
stream.on('error', function (error) {
if (signal) {
signal.removeEventListener('abort', abortHandler);
}
reject(error);
});
})];
});
});
};
UploadxClient.prototype.uploadBufferInChunks = function (url, buffer, start, totalSize, onProgress, signal) {
return __awaiter(this, void 0, void 0, function () {
var view, position, end, chunk;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
view = buffer instanceof ArrayBuffer ? new Uint8Array(buffer) : buffer;
position = start;
_a.label = 1;
case 1:
if (!(position < totalSize)) return [3 /*break*/, 3];
// Check if the operation was aborted
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
throw new AbortError();
}
end = Math.min(position + this.chunkSize, totalSize);
chunk = view.subarray(position, end);
return [4 /*yield*/, this.uploadChunk(url, chunk, position, end, totalSize, onProgress, signal)];
case 2:
_a.sent();
position = end;
return [3 /*break*/, 1];
case 3: return [2 /*return*/];
}
});
});
};
UploadxClient.prototype.isBlob = function (data) {
return typeof Blob !== 'undefined' && data instanceof Blob;
};
UploadxClient.prototype.isStream = function (data) {
return !!data && typeof data.pipe === 'function';
};
UploadxClient.prototype.parseRangeHeader = function (range) {
if (!range)
return 0;
var matches = range.match(/bytes=\d+-(\d+)/);
return matches ? Number.parseInt(matches[1], 10) + 1 : 0;
};
UploadxClient.prototype.handleError = function (error, context) {
var _a, _b, _c;
if (error instanceof AbortError) {
return error;
}
if (axios_1.default.isAxiosError(error)) {
return new Error("".concat(context, ": ").concat(error.message, " ").concat(((_c = (_b = (_a = error.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.error) === null || _c === void 0 ? void 0 : _c.message) || error.cause, " "));
}
if (error instanceof Error) {
return new Error("".concat(context, ": ").concat(error.message));
}
return new Error(context);
};
return UploadxClient;
}());
exports.UploadxClient = UploadxClient;