uploadzx
Version:
Browser-only TypeScript upload library with tus integration for resumable uploads
1,534 lines (1,525 loc) • 51.2 kB
JavaScript
'use strict';
var react = require('react');
var tusJsClient = require('tus-js-client');
var jsxRuntime = require('react/jsx-runtime');
// src/react/hooks/useUploadzx.ts
// src/utils/index.ts
function generateFileId() {
return crypto.randomUUID();
}
function isFileSystemAccessSupported() {
return typeof window !== "undefined" && "showOpenFilePicker" in window && "showSaveFilePicker" in window && "showDirectoryPicker" in window;
}
function createMockFileHandle(file) {
const mockHandle = {
kind: "file",
name: file.name,
getFile: async () => file,
queryPermission: async () => "granted",
requestPermission: async () => "granted",
createWritable: async () => {
throw new Error("Write operations not supported in Safari fallback mode");
},
isSameEntry: async () => false
};
return mockHandle;
}
async function getFileHandlesFromDataTransfer(dataTransfer) {
const items = Array.from(dataTransfer.items).filter((item) => item.kind === "file");
const results = [];
for (const item of items) {
try {
if (item.getAsFileSystemHandle && isFileSystemAccessSupported()) {
const handle = await item.getAsFileSystemHandle();
if (handle && handle.kind === "file") {
const file2 = await handle.getFile();
results.push({ file: file2, handle });
continue;
}
}
} catch (error) {
console.warn("Failed to get file handle from drag item:", error);
}
const file = item.getAsFile();
if (file) {
const mockHandle = createMockFileHandle(file);
results.push({ file, handle: mockHandle });
}
}
return results;
}
async function getFilesFromDragEvent(event) {
if (!event.dataTransfer) {
return [];
}
return getFileHandlesFromDataTransfer(event.dataTransfer);
}
// src/core/FilePicker.ts
var FilePicker = class {
constructor(options = {}) {
this.options = {
multiple: true,
useFileSystemAccess: false,
...options
};
}
generateUUID() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c == "x" ? r : r & 3 | 8;
return v.toString(16);
});
}
async pickFiles() {
if (this.options.useFileSystemAccess && isFileSystemAccessSupported()) {
console.log("Picking files with file system access");
return this.pickWithFileSystemAccess();
} else if (this.options.useFileSystemAccess && !isFileSystemAccessSupported()) {
console.log("Picking files with input (Safari fallback for File System Access)");
return this.pickWithInputAndMockHandles();
}
console.log("Picking files with input");
return this.pickWithInput();
}
async pickWithFileSystemAccess() {
try {
const fileHandles = await window.showOpenFilePicker({
multiple: this.options.multiple,
types: this.options.accept ? [
{
description: "Files",
accept: { "*/*": [this.options.accept] }
}
] : void 0
});
const uploadFiles = [];
for (const fileHandle of fileHandles) {
const file = await fileHandle.getFile();
uploadFiles.push({
id: this.generateUUID(),
file,
fileHandle,
name: file.name,
size: file.size,
type: file.type
});
}
return uploadFiles;
} catch (error) {
if (error.name === "AbortError") {
return [];
}
throw error;
}
}
async pickWithInputAndMockHandles() {
return new Promise((resolve) => {
const input = document.createElement("input");
input.type = "file";
input.multiple = this.options.multiple || false;
if (this.options.accept) {
input.accept = this.options.accept;
}
input.onchange = () => {
const files = Array.from(input.files || []);
const uploadFiles = files.map((file) => {
const mockHandle = this.createMockFileHandle(file);
return {
id: this.generateUUID(),
file,
fileHandle: mockHandle,
name: file.name,
size: file.size,
type: file.type
};
});
resolve(uploadFiles);
};
input.click();
});
}
createMockFileHandle(file) {
const mockHandle = {
kind: "file",
name: file.name,
getFile: async () => file,
queryPermission: async () => "granted",
requestPermission: async () => "granted",
createWritable: async () => {
throw new Error("Write operations not supported in Safari fallback mode");
},
isSameEntry: async () => false
};
return mockHandle;
}
async pickWithInput() {
return new Promise((resolve) => {
const input = document.createElement("input");
input.type = "file";
input.multiple = this.options.multiple || false;
if (this.options.accept) {
input.accept = this.options.accept;
}
input.onchange = () => {
const files = Array.from(input.files || []);
const uploadFiles = files.map((file) => ({
id: this.generateUUID(),
file,
name: file.name,
size: file.size,
type: file.type
}));
resolve(uploadFiles);
};
input.click();
});
}
};
var TusUploader = class {
constructor(uploadFile, options, events = {}, tusOptions) {
this.uploadFile = uploadFile;
this.options = options;
this.events = events;
this.abortController = new AbortController();
this.previousUploadUrl = tusOptions?.previousUploadUrl;
this.trackSpeed = tusOptions?.trackSpeed ?? false;
const initialBytesUploaded = tusOptions?.previousBytesUploaded || 0;
const initialPercentage = Math.round(initialBytesUploaded / uploadFile.size * 100);
this.state = {
fileId: uploadFile.id,
status: this.previousUploadUrl ? "paused" : "pending",
file: uploadFile.file,
progress: {
fileId: uploadFile.id,
bytesUploaded: initialBytesUploaded,
bytesTotal: uploadFile.size,
percentage: initialPercentage,
bytesPerSecond: 0
}
};
}
async start() {
if (this.state.status === "uploading") {
return;
}
this.updateState({ status: "uploading" });
if (this.trackSpeed) {
this.lastProgressUpdate = {
bytesUploaded: this.state.progress.bytesUploaded,
timestamp: Date.now()
};
}
try {
await this.startTusUpload();
} catch (error) {
this.updateState({
status: "error",
error
});
this.events.onError?.(this.uploadFile.id, error);
}
}
async pause() {
if (this.upload && this.state.status === "uploading") {
this.previousUploadUrl = this.upload.url || void 0;
this.upload.abort();
this.updateState({ status: "paused" });
if (this.trackSpeed) {
this.lastProgressUpdate = void 0;
}
}
}
async resume() {
if (this.canResume()) {
console.log(
`Resuming upload for file: ${this.uploadFile.name}, previousUrl: ${this.previousUploadUrl}`
);
this.abortController = new AbortController();
await this.start();
}
}
async cancel() {
if (this.state.status === "cancelled") {
return;
}
this.abortController.abort();
if (this.upload) {
this.upload.abort();
}
this.previousUploadUrl = void 0;
if (this.trackSpeed) {
this.lastProgressUpdate = void 0;
}
this.updateState({ status: "cancelled" });
this.events.onCancel?.(this.uploadFile.id);
}
getState() {
return { ...this.state };
}
getCurrentUploadUrl() {
return this.upload?.url || this.previousUploadUrl;
}
canResume() {
return this.state.status === "paused" || this.state.status === "error";
}
updateState(updates) {
this.state = { ...this.state, ...updates };
this.events.onStateChange?.(this.state);
}
updateProgress(bytesUploaded) {
const percentage = Number((bytesUploaded / this.uploadFile.size * 100).toFixed(2));
let bytesPerSecond = 0;
if (this.trackSpeed) {
const now = Date.now();
if (this.lastProgressUpdate) {
const timeDiff = (now - this.lastProgressUpdate.timestamp) / 1e3;
const bytesDiff = bytesUploaded - this.lastProgressUpdate.bytesUploaded;
const shouldUpdateSpeed = timeDiff > 1 && bytesDiff > 0;
if (shouldUpdateSpeed) {
bytesPerSecond = Math.round(bytesDiff / timeDiff);
this.lastProgressUpdate = {
bytesUploaded,
timestamp: now
};
} else {
bytesPerSecond = this.state.progress.bytesPerSecond;
}
}
}
const progress = {
fileId: this.uploadFile.id,
bytesUploaded,
bytesTotal: this.uploadFile.size,
percentage,
bytesPerSecond
};
this.updateState({ progress });
this.events.onProgress?.(progress);
}
async startTusUpload() {
return new Promise(async (resolve, reject) => {
const uploadOptions = {
endpoint: this.options.endpoint,
uploadUrl: this.previousUploadUrl,
// Use previous URL if resuming
chunkSize: this.options.chunkSize || 1024 * 1024,
// 1MB default
retryDelays: this.options.retryDelays || [0, 3e3, 5e3, 1e4, 2e4],
metadata: {
filename: this.uploadFile.name,
filetype: this.uploadFile.type,
...this.options.metadata
},
headers: this.options.headers,
// Use default fingerprinting for resumable uploads (file size + modification time + name)
fingerprint: tusJsClient.defaultOptions.fingerprint,
// Ensure fingerprints are stored for resuming
storeFingerprintForResuming: true,
removeFingerprintOnSuccess: true,
onError: (error) => {
this.updateState({
status: "error",
error
});
this.events.onError?.(this.uploadFile.id, error);
reject(error);
},
onProgress: (bytesUploaded, bytesTotal) => {
this.updateProgress(bytesUploaded);
},
onSuccess: () => {
const tusUrl = this.upload?.url || void 0;
this.updateState({
status: "completed",
tusUrl
});
this.events.onComplete?.(this.uploadFile.id, tusUrl || "");
this.previousUploadUrl = void 0;
resolve();
}
};
if (this.abortController.signal.aborted) {
reject(new Error("Upload was aborted"));
return;
}
this.abortController.signal.addEventListener("abort", () => {
if (this.upload) {
this.upload.abort();
}
});
this.upload = new tusJsClient.Upload(this.uploadFile.file, uploadOptions);
if (this.previousUploadUrl) {
try {
const previousUploads = await this.upload.findPreviousUploads();
console.log("Found previous uploads:", previousUploads.length);
const matchingUpload = previousUploads.find(
(upload) => upload.uploadUrl === this.previousUploadUrl
);
if (matchingUpload) {
console.log("Resuming from previous upload:", {
url: matchingUpload.uploadUrl,
size: matchingUpload.size,
uploaded: matchingUpload.size ? this.state.progress.bytesUploaded : 0
});
this.upload.resumeFromPreviousUpload(matchingUpload);
} else {
console.warn("Previous upload not found, starting new upload");
}
} catch (error) {
console.warn("Could not find previous upload, starting new:", error);
}
}
this.upload.start();
});
}
};
// src/core/FileHandleStore.ts
var FileHandleStore = class {
constructor() {
this.dbName = "uploadzx-filehandles";
this.version = 2;
// increment this for safari fallback support
this.storeName = "filehandles";
this.safariStoreName = "safari-files";
this.isFileSystemAccessSupported = isFileSystemAccessSupported();
}
async openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains(this.storeName)) {
const store = db.createObjectStore(this.storeName, { keyPath: "id" });
store.createIndex("name", "name", { unique: false });
}
if (!db.objectStoreNames.contains(this.safariStoreName)) {
const safariStore = db.createObjectStore(this.safariStoreName, { keyPath: "id" });
safariStore.createIndex("name", "name", { unique: false });
}
};
});
}
async storeFileHandle(fileHandle, id) {
if (this.isFileSystemAccessSupported) {
return this.storeNativeFileHandle(fileHandle, id);
} else {
const file = await fileHandle.getFile();
return this.storeSafariFile(file, id);
}
}
async storeNativeFileHandle(fileHandle, id) {
const db = await this.openDB();
const file = await fileHandle.getFile();
const storedHandle = {
id,
name: file.name,
size: file.size,
type: file.type,
handle: fileHandle,
lastModified: file.lastModified
};
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], "readwrite");
const store = transaction.objectStore(this.storeName);
const request = store.put(storedHandle);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
async storeSafariFile(file, id) {
const db = await this.openDB();
const data = await file.arrayBuffer();
const safariFile = {
id,
name: file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified,
data
};
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.safariStoreName], "readwrite");
const store = transaction.objectStore(this.safariStoreName);
const request = store.put(safariFile);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
async getFileHandle(id) {
if (this.isFileSystemAccessSupported) {
return this.getNativeFileHandle(id);
} else {
return this.getSafariFileAsHandle(id);
}
}
async getNativeFileHandle(id) {
const db = await this.openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], "readonly");
const store = transaction.objectStore(this.storeName);
const request = store.get(id);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result || null);
});
}
async getSafariFileAsHandle(id) {
const db = await this.openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.safariStoreName], "readonly");
const store = transaction.objectStore(this.safariStoreName);
const request = store.get(id);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const safariFile = request.result;
if (!safariFile) {
resolve(null);
return;
}
const mockHandle = this.createMockFileHandle(safariFile);
const storedHandle = {
id: safariFile.id,
name: safariFile.name,
size: safariFile.size,
type: safariFile.type,
handle: mockHandle,
lastModified: safariFile.lastModified,
tusUploadUrl: safariFile.tusUploadUrl,
bytesUploaded: safariFile.bytesUploaded
};
resolve(storedHandle);
};
});
}
createMockFileHandle(safariFile) {
const file = new File([safariFile.data], safariFile.name, {
type: safariFile.type,
lastModified: safariFile.lastModified
});
const mockHandle = {
kind: "file",
name: safariFile.name,
getFile: async () => file,
queryPermission: async () => "granted",
requestPermission: async () => "granted",
createWritable: async () => {
throw new Error("Write operations not supported in Safari fallback mode");
},
isSameEntry: async () => false
};
return mockHandle;
}
async getAllFileHandles() {
if (this.isFileSystemAccessSupported) {
return this.getAllNativeFileHandles();
} else {
return this.getAllSafariFilesAsHandles();
}
}
async getAllNativeFileHandles() {
const db = await this.openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], "readonly");
const store = transaction.objectStore(this.storeName);
const request = store.getAll();
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
async getAllSafariFilesAsHandles() {
const db = await this.openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.safariStoreName], "readonly");
const store = transaction.objectStore(this.safariStoreName);
const request = store.getAll();
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const safariFiles = request.result;
const handles = safariFiles.map((safariFile) => {
const mockHandle = this.createMockFileHandle(safariFile);
return {
id: safariFile.id,
name: safariFile.name,
size: safariFile.size,
type: safariFile.type,
handle: mockHandle,
lastModified: safariFile.lastModified,
tusUploadUrl: safariFile.tusUploadUrl,
bytesUploaded: safariFile.bytesUploaded
};
});
resolve(handles);
};
});
}
async removeFileHandle(id) {
console.log("removeFileHandle", id);
const db = await this.openDB();
const storeName = this.isFileSystemAccessSupported ? this.storeName : this.safariStoreName;
return new Promise((resolve, reject) => {
const transaction = db.transaction([storeName], "readwrite");
const store = transaction.objectStore(storeName);
const request = store.delete(id);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
async updateFileHandleProgress(id, tusUploadUrl, bytesUploaded) {
if (this.isFileSystemAccessSupported) {
return this.updateNativeFileHandleProgress(id, tusUploadUrl, bytesUploaded);
} else {
return this.updateSafariFileHandleProgress(id, tusUploadUrl, bytesUploaded);
}
}
async updateNativeFileHandleProgress(id, tusUploadUrl, bytesUploaded) {
const db = await this.openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.storeName], "readwrite");
const store = transaction.objectStore(this.storeName);
const getRequest = store.get(id);
getRequest.onerror = () => reject(getRequest.error);
getRequest.onsuccess = () => {
const storedHandle = getRequest.result;
if (storedHandle) {
storedHandle.tusUploadUrl = tusUploadUrl;
storedHandle.bytesUploaded = bytesUploaded;
const putRequest = store.put(storedHandle);
putRequest.onerror = () => reject(putRequest.error);
putRequest.onsuccess = () => resolve();
} else {
resolve();
}
};
});
}
async updateSafariFileHandleProgress(id, tusUploadUrl, bytesUploaded) {
const db = await this.openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.safariStoreName], "readwrite");
const store = transaction.objectStore(this.safariStoreName);
const getRequest = store.get(id);
getRequest.onerror = () => reject(getRequest.error);
getRequest.onsuccess = () => {
const safariFile = getRequest.result;
if (safariFile) {
safariFile.tusUploadUrl = tusUploadUrl;
safariFile.bytesUploaded = bytesUploaded;
const putRequest = store.put(safariFile);
putRequest.onerror = () => reject(putRequest.error);
putRequest.onsuccess = () => resolve();
} else {
resolve();
}
};
});
}
async verifyPermission(fileHandle) {
if (!this.isFileSystemAccessSupported) {
return true;
}
try {
const permission = await fileHandle.queryPermission({ mode: "read" });
if (permission === "granted") {
return true;
}
if (permission === "prompt") {
const requestPermission = await fileHandle.requestPermission({ mode: "read" });
return requestPermission === "granted";
}
return false;
} catch (error) {
console.error("Error verifying permission:", error);
return false;
}
}
async getFileFromHandleByID(id) {
console.log("getFileFromHandleByID", id);
if (!this.isFileSystemAccessSupported) {
return this.getSafariFileByID(id);
}
const fileHandle = await this.getFileHandle(id);
if (!fileHandle) {
return null;
}
const hasPermission = await this.verifyPermission(fileHandle.handle);
if (!hasPermission) {
try {
console.log("requesting permission", fileHandle.id);
await fileHandle.handle.requestPermission({ mode: "read" });
} catch {
console.log("error requesting permission", fileHandle.id);
await this.removeFileHandle(fileHandle.id);
return null;
}
}
try {
const file = await fileHandle.handle.getFile();
return file;
} catch (error) {
return null;
}
}
async getSafariFileByID(id) {
const db = await this.openDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([this.safariStoreName], "readonly");
const store = transaction.objectStore(this.safariStoreName);
const request = store.get(id);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const safariFile = request.result;
if (!safariFile) {
resolve(null);
return;
}
const file = new File([safariFile.data], safariFile.name, {
type: safariFile.type,
lastModified: safariFile.lastModified
});
resolve(file);
};
});
}
async clear() {
console.log("clear");
const db = await this.openDB();
const storeNames = this.isFileSystemAccessSupported ? [this.storeName] : [this.storeName, this.safariStoreName];
const transaction = db.transaction(storeNames, "readwrite");
storeNames.forEach((storeName) => {
if (db.objectStoreNames.contains(storeName)) {
const store = transaction.objectStore(storeName);
store.clear();
}
});
}
};
// src/core/UploadQueue.ts
var UploadQueue = class {
constructor(options, events = {}) {
this.uploaders = /* @__PURE__ */ new Map();
this.unfinishedUploads = /* @__PURE__ */ new Map();
this.queue = [];
this.activeUploads = /* @__PURE__ */ new Set();
this.isInitialized = false;
console.log("UploadQueue constructor", options, events);
this.options = {
maxConcurrent: 3,
autoStart: false,
...options
};
this.events = events;
this.fileHandleStore = new FileHandleStore();
this.initialize();
}
async initialize() {
try {
console.log("initialize");
await this.getUnfinishedUploadsFromStore();
console.log("initialize 2");
this.isInitialized = true;
console.log("initialize 3");
this.options.onInit?.();
console.log("initialize 4");
} catch (error) {
console.error("Failed to initialize UploadQueue:", error);
console.log("initialize 5");
this.options.onInit?.();
}
}
getIsInitialized() {
return this.isInitialized;
}
async addFiles(files, tusOptions) {
for (const file of files) {
const uploader = new TusUploader(
file,
this.options,
{
onProgress: this.events.onProgress,
onStateChange: (state) => {
this.handleStateChange(state);
},
onComplete: (fileId, tusUrl) => {
this.handleComplete(fileId, tusUrl);
this.fileHandleStore.removeFileHandle(fileId);
},
onError: (fileId, error) => {
this.handleError(fileId, error);
},
onCancel: (fileId) => {
this.cancelUpload(fileId);
}
},
tusOptions
);
this.uploaders.set(file.id, uploader);
this.queue.push(file.id);
if (file.fileHandle) {
await this.fileHandleStore.storeFileHandle(file.fileHandle, file.id);
}
}
if (this.options.autoStart) {
this.processQueue();
}
}
async startQueue() {
this.processQueue();
}
async pauseAll() {
for (const uploader of this.uploaders.values()) {
await uploader.pause();
await this.updateStoredFileHandleProgress(uploader);
}
}
async resumeAll() {
for (const uploader of this.uploaders.values()) {
const state = uploader.getState();
if (state.status === "paused") {
await uploader.resume();
}
}
this.processQueue();
}
async cancelAll() {
for (const uploader of this.uploaders.values()) {
await uploader.cancel();
}
this.activeUploads.clear();
this.queue.length = 0;
this.fileHandleStore.clear();
}
async pauseUpload(fileId) {
const uploader = this.uploaders.get(fileId);
if (uploader) {
await uploader.pause();
await this.updateStoredFileHandleProgress(uploader);
}
}
async resumeUpload(fileId) {
const uploader = this.uploaders.get(fileId);
if (uploader) {
await uploader.resume();
}
}
async cancelUpload(fileId) {
console.log("cancelUpload", fileId);
const uploader = this.uploaders.get(fileId);
console.log("uploader", uploader);
if (uploader) {
await uploader.cancel();
this.activeUploads.delete(fileId);
this.removeFromQueue(fileId);
this.processQueue();
this.fileHandleStore.removeFileHandle(fileId);
}
}
clearCompletedUploads() {
for (const fileId of this.uploaders.keys()) {
if (this.activeUploads.has(fileId)) {
this.uploaders.delete(fileId);
}
}
}
getUploadState(fileId) {
const uploader = this.uploaders.get(fileId);
return uploader ? uploader.getState() : null;
}
getAllStates() {
return Array.from(this.uploaders.values()).map((uploader) => uploader.getState());
}
getQueueLength() {
return this.queue.length;
}
getActiveCount() {
return this.activeUploads.size;
}
async getUnfinishedUploads() {
return Array.from(this.unfinishedUploads.values());
}
async restoreUnfinishedUpload(fileHandleOrId, tusOpts) {
return await this.resumeUnfinishedUpload(fileHandleOrId, tusOpts);
}
async updateStoredFileHandleProgress(uploader) {
const state = uploader.getState();
const uploadUrl = uploader.getCurrentUploadUrl();
if (uploadUrl && state.progress.bytesUploaded > 0) {
const storedHandle = this.unfinishedUploads.get(state.fileId);
if (storedHandle) {
const updatedHandle = {
...storedHandle,
tusUploadUrl: uploadUrl,
bytesUploaded: state.progress.bytesUploaded
};
this.unfinishedUploads.set(state.fileId, updatedHandle);
await this.fileHandleStore.updateFileHandleProgress(
state.fileId,
uploadUrl,
state.progress.bytesUploaded
);
}
}
}
async processQueue() {
const maxConcurrent = this.options.maxConcurrent || 3;
while (this.queue.length > 0 && this.activeUploads.size < maxConcurrent) {
const fileId = this.queue.shift();
if (!fileId) continue;
const uploader = this.uploaders.get(fileId);
if (!uploader) continue;
const state = uploader.getState();
if (state.status === "pending" || state.status === "paused") {
this.activeUploads.add(fileId);
uploader.start().catch(() => {
});
}
}
}
handleStateChange(state) {
if (state.status === "uploading" || state.status === "paused") {
const uploader = this.uploaders.get(state.fileId);
if (uploader) {
this.updateStoredFileHandleProgress(uploader);
}
}
this.events.onStateChange?.(state);
}
handleComplete(fileId, tusUrl) {
this.activeUploads.delete(fileId);
this.unfinishedUploads.delete(fileId);
this.events.onComplete?.(fileId, tusUrl);
this.processQueue();
}
handleError(fileId, error) {
this.activeUploads.delete(fileId);
const uploader = this.uploaders.get(fileId);
if (uploader) {
this.updateStoredFileHandleProgress(uploader);
}
this.events.onError?.(fileId, error);
this.processQueue();
}
removeFromQueue(fileId) {
const index = this.queue.indexOf(fileId);
if (index > -1) {
this.queue.splice(index, 1);
}
}
async getUnfinishedUploadsFromStore() {
const fileHandles = await this.fileHandleStore.getAllFileHandles();
this.unfinishedUploads = new Map(
fileHandles.filter((fileHandle) => !this.activeUploads.has(fileHandle.id)).map((fileHandle) => [fileHandle.id, fileHandle])
);
return;
}
async resumeUnfinishedUpload(fileHandleOrId, tusOpts) {
let id;
let fileHandle;
if (typeof fileHandleOrId === "string") {
id = fileHandleOrId;
fileHandle = this.unfinishedUploads.get(id);
console.log("fileHandle", fileHandle);
} else {
id = fileHandleOrId.id;
fileHandle = fileHandleOrId;
}
if (!fileHandle) {
console.log("fileHandle not found");
return;
}
const file = await this.fileHandleStore.getFileFromHandleByID(id);
console.log("file restored", file);
if (!file) {
console.log("file not found");
return;
}
if (file.lastModified !== fileHandle.lastModified) {
console.warn(`File modified since last access: ${fileHandle.name}. Removing from storage.`);
await this.fileHandleStore.removeFileHandle(fileHandle.id);
return;
}
console.log("file found", file);
const tusOptions = {
previousUploadUrl: fileHandle.tusUploadUrl,
previousBytesUploaded: fileHandle.bytesUploaded || 0,
trackSpeed: tusOpts?.trackSpeed || false
};
const uploader = new TusUploader(
{
id: fileHandle.id,
file,
fileHandle: fileHandle.handle,
name: fileHandle.name,
size: fileHandle.size,
type: fileHandle.type
},
this.options,
{
onProgress: this.events.onProgress,
onStateChange: (state) => {
this.handleStateChange(state);
},
onComplete: (fileId, tusUrl) => {
this.handleComplete(fileId, tusUrl);
this.fileHandleStore.removeFileHandle(fileId);
},
onError: (fileId, error) => {
this.handleError(fileId, error);
},
onCancel: (fileId) => {
this.cancelUpload(fileId);
}
},
tusOptions
);
this.uploaders.set(fileHandle.id, uploader);
if (this.options.autoStart) {
this.queue.push(fileHandle.id);
this.processQueue();
}
}
};
// src/index.ts
var Uploadzx = class {
constructor(options, events = {}) {
this.filePicker = new FilePicker(options.filePickerOptions);
this.uploadQueue = new UploadQueue(options, events);
this.tusOptions = options.tusOptions;
}
getIsInitialized() {
return this.uploadQueue.getIsInitialized();
}
async pickAndUploadFiles(tusOptions) {
const files = await this.filePicker.pickFiles();
if (files.length > 0) {
await this.uploadQueue.addFiles(files, tusOptions || this.tusOptions);
}
}
async pickFiles() {
return this.filePicker.pickFiles();
}
async addFiles(files, tusOptions) {
return this.uploadQueue.addFiles(files, tusOptions || this.tusOptions);
}
async startUploads() {
return this.uploadQueue.startQueue();
}
async pauseAll() {
return this.uploadQueue.pauseAll();
}
async resumeAll() {
return this.uploadQueue.resumeAll();
}
async cancelAll() {
console.log("cancelAll");
return this.uploadQueue.cancelAll();
}
async pauseUpload(fileId) {
return this.uploadQueue.pauseUpload(fileId);
}
async resumeUpload(fileId) {
return this.uploadQueue.resumeUpload(fileId);
}
async cancelUpload(fileId) {
return this.uploadQueue.cancelUpload(fileId);
}
async restoreUnfinishedUpload(fileHandleOrId, tusOpts) {
return this.uploadQueue.restoreUnfinishedUpload(fileHandleOrId, tusOpts || this.tusOptions);
}
async clearCompletedUploads() {
return this.uploadQueue.clearCompletedUploads();
}
getUploadState(fileId) {
return this.uploadQueue.getUploadState(fileId);
}
getAllStates() {
return this.uploadQueue.getAllStates();
}
getQueueStats() {
return {
queueLength: this.uploadQueue.getQueueLength(),
activeCount: this.uploadQueue.getActiveCount()
};
}
async getUnfinishedUploads() {
return this.uploadQueue.getUnfinishedUploads();
}
};
// src/react/hooks/useUploadzx.ts
function useUploadzx(options) {
const [isInitialized, setIsInitialized] = react.useState(false);
const [uploadStates, setUploadStates] = react.useState({});
const [queueStats, setQueueStats] = react.useState({ queueLength: 0, activeCount: 0 });
const [unfinishedUploads, setUnfinishedUploads] = react.useState([]);
const uploadzxRef = react.useRef(null);
const mountedRef = react.useRef(true);
const events = react.useMemo(
() => ({
onProgress: options.onProgress,
onStateChange: (state) => {
if (mountedRef.current) {
setUploadStates((prev) => ({
...prev,
[state.fileId]: state
}));
if (unfinishedUploads.find((upload) => upload.id === state.fileId)) {
setUnfinishedUploads((prev) => {
const newUnfinishedUploads = prev.filter((upload) => upload.id !== state.fileId);
return newUnfinishedUploads;
});
}
options.onStateChange?.(state);
}
},
onComplete: options.onComplete,
onError: options.onError,
onCancel: options.onCancel
}),
[
options.onProgress,
options.onStateChange,
options.onComplete,
options.onError,
options.onCancel
]
);
react.useEffect(() => {
mountedRef.current = true;
const initializeUploadzx = async () => {
if (uploadzxRef.current) {
return;
}
uploadzxRef.current = new Uploadzx(
{
...options,
onInit: async () => {
console.log("onInit prop");
if (mountedRef.current) {
setIsInitialized(true);
if (uploadzxRef.current) {
try {
const uploads = await uploadzxRef.current.getUnfinishedUploads();
if (mountedRef.current) {
console.log("uploads", uploads);
setUnfinishedUploads(uploads);
const stats = uploadzxRef.current.getQueueStats();
setQueueStats(stats);
}
} catch (error) {
console.error("Error fetching unfinished uploads:", error);
}
}
}
options.onInit?.();
}
},
events
);
};
initializeUploadzx();
return () => {
mountedRef.current = false;
if (uploadzxRef.current) {
uploadzxRef.current = null;
}
setIsInitialized(false);
setUploadStates({});
setQueueStats({ queueLength: 0, activeCount: 0 });
setUnfinishedUploads([]);
};
}, []);
const pickAndUploadFiles = react.useCallback(async () => {
if (!uploadzxRef.current) return;
await uploadzxRef.current.pickAndUploadFiles();
const stats = uploadzxRef.current.getQueueStats();
setQueueStats(stats);
}, [uploadzxRef.current]);
const pickFiles = react.useCallback(async () => {
if (!uploadzxRef.current) return [];
return await uploadzxRef.current.pickFiles();
}, [uploadzxRef.current]);
const addFiles = react.useCallback(async (files) => {
if (!uploadzxRef.current) return;
await uploadzxRef.current.addFiles(files);
const stats = uploadzxRef.current.getQueueStats();
setQueueStats(stats);
}, [uploadzxRef.current]);
const startUploads = react.useCallback(async () => {
if (!uploadzxRef.current) return;
await uploadzxRef.current.startUploads();
const stats = uploadzxRef.current.getQueueStats();
setQueueStats(stats);
}, [uploadzxRef.current]);
const pauseAll = react.useCallback(async () => {
if (!uploadzxRef.current) return;
await uploadzxRef.current.pauseAll();
}, [uploadzxRef.current]);
const resumeAll = react.useCallback(async () => {
if (!uploadzxRef.current) return;
await uploadzxRef.current.resumeAll();
}, [uploadzxRef.current]);
const cancelAll = react.useCallback(async () => {
if (!uploadzxRef.current) return;
await uploadzxRef.current.cancelAll();
}, [uploadzxRef.current]);
const pauseUpload = react.useCallback(async (fileId) => {
if (!uploadzxRef.current) return;
await uploadzxRef.current.pauseUpload(fileId);
}, [uploadzxRef.current]);
const resumeUpload = react.useCallback(async (fileId) => {
if (!uploadzxRef.current) return;
await uploadzxRef.current.resumeUpload(fileId);
}, [uploadzxRef.current]);
const cancelUpload = react.useCallback(async (fileId) => {
if (!uploadzxRef.current) return;
await uploadzxRef.current.cancelUpload(fileId);
const stats = uploadzxRef.current.getQueueStats();
setQueueStats(stats);
}, [uploadzxRef.current]);
const getUploadState = react.useCallback((fileId) => {
if (!uploadzxRef.current) return null;
return uploadzxRef.current.getUploadState(fileId);
}, [uploadzxRef.current]);
const getAllStates = react.useCallback(() => {
if (!uploadzxRef.current) return {};
return uploadzxRef.current.getAllStates();
}, [uploadzxRef.current]);
const clearCompletedUploads = react.useCallback(() => {
if (!uploadzxRef.current) return;
uploadzxRef.current.clearCompletedUploads();
setUploadStates({});
}, [uploadzxRef.current]);
const restoreUnfinishedUpload = react.useCallback(async (fileHandleOrId) => {
if (!uploadzxRef.current) return;
await uploadzxRef.current.restoreUnfinishedUpload(fileHandleOrId);
setUnfinishedUploads((prev) => {
if (typeof fileHandleOrId === "string") {
const newUnfinishedUploads2 = prev.filter((upload) => upload.id !== fileHandleOrId);
const stats2 = uploadzxRef.current?.getQueueStats();
if (stats2) {
setQueueStats(stats2);
}
return newUnfinishedUploads2;
}
const newUnfinishedUploads = prev.filter((upload) => upload.id !== fileHandleOrId.id);
const stats = uploadzxRef.current?.getQueueStats();
if (stats) {
setQueueStats(stats);
}
return newUnfinishedUploads;
});
}, [uploadzxRef.current]);
const value = react.useMemo(() => {
return {
isInitialized,
uploadStates,
queueStats,
unfinishedUploads,
pickAndUploadFiles,
pickFiles,
addFiles,
startUploads,
pauseAll,
resumeAll,
cancelAll,
pauseUpload,
resumeUpload,
cancelUpload,
getUploadState,
getAllStates,
restoreUnfinishedUpload,
clearCompletedUploads
};
}, [
isInitialized,
uploadStates,
queueStats,
unfinishedUploads,
pickAndUploadFiles,
pickFiles,
addFiles,
startUploads,
pauseAll,
resumeAll,
cancelAll,
pauseUpload,
resumeUpload,
cancelUpload,
getUploadState,
getAllStates,
restoreUnfinishedUpload,
clearCompletedUploads
]);
return value;
}
var UploadzxActionsContext = react.createContext(null);
var UploadzxStateContext = react.createContext(null);
var UploadStatesContext = react.createContext(null);
var QueueStatsContext = react.createContext(null);
var UnfinishedUploadsContext = react.createContext(null);
function UploadzxProvider({ children, options }) {
const uploadzxValue = useUploadzx(options);
const actionsValue = react.useMemo(() => ({
pickAndUploadFiles: uploadzxValue.pickAndUploadFiles,
pickFiles: uploadzxValue.pickFiles,
addFiles: uploadzxValue.addFiles,
startUploads: uploadzxValue.startUploads,
pauseAll: uploadzxValue.pauseAll,
resumeAll: uploadzxValue.resumeAll,
cancelAll: uploadzxValue.cancelAll,
pauseUpload: uploadzxValue.pauseUpload,
resumeUpload: uploadzxValue.resumeUpload,
cancelUpload: uploadzxValue.cancelUpload,
getUploadState: uploadzxValue.getUploadState,
getAllStates: uploadzxValue.getAllStates,
clearCompletedUploads: uploadzxValue.clearCompletedUploads,
restoreUnfinishedUpload: uploadzxValue.restoreUnfinishedUpload
}), [
uploadzxValue.pickAndUploadFiles,
uploadzxValue.pickFiles,
uploadzxValue.addFiles,
uploadzxValue.startUploads,
uploadzxValue.pauseAll,
uploadzxValue.resumeAll,
uploadzxValue.cancelAll,
uploadzxValue.pauseUpload,
uploadzxValue.resumeUpload,
uploadzxValue.cancelUpload,
uploadzxValue.getUploadState,
uploadzxValue.getAllStates,
uploadzxValue.clearCompletedUploads,
uploadzxValue.restoreUnfinishedUpload
]);
const stateValue = react.useMemo(() => ({
isInitialized: uploadzxValue.isInitialized
}), [uploadzxValue.isInitialized]);
const uploadStatesValue = react.useMemo(() => ({
uploadStates: uploadzxValue.uploadStates
}), [uploadzxValue.uploadStates]);
const queueStatsValue = react.useMemo(() => ({
queueStats: uploadzxValue.queueStats
}), [uploadzxValue.queueStats]);
const unfinishedUploadsValue = react.useMemo(() => ({
unfinishedUploads: uploadzxValue.unfinishedUploads
}), [uploadzxValue.unfinishedUploads]);
return /* @__PURE__ */ jsxRuntime.jsx(UploadzxActionsContext.Provider, { value: actionsValue, children: /* @__PURE__ */ jsxRuntime.jsx(UploadzxStateContext.Provider, { value: stateValue, children: /* @__PURE__ */ jsxRuntime.jsx(UploadStatesContext.Provider, { value: uploadStatesValue, children: /* @__PURE__ */ jsxRuntime.jsx(QueueStatsContext.Provider, { value: queueStatsValue, children: /* @__PURE__ */ jsxRuntime.jsx(UnfinishedUploadsContext.Provider, { value: unfinishedUploadsValue, children }) }) }) }) });
}
function useUploadzxActions() {
const context = react.useContext(UploadzxActionsContext);
if (!context) {
throw new Error("useUploadzxActions must be used within UploadzxProvider");
}
return context;
}
function useUploadzxState() {
const context = react.useContext(UploadzxStateContext);
if (!context) {
throw new Error("useUploadzxState must be used within UploadzxProvider");
}
return context;
}
function useUploadStates() {
const context = react.useContext(UploadStatesContext);
if (!context) {
throw new Error("useUploadStates must be used within UploadzxProvider");
}
return context;
}
function useQueueStats() {
const context = react.useContext(QueueStatsContext);
if (!context) {
throw new Error("useQueueStats must be used within UploadzxProvider");
}
return context;
}
function useUnfinishedUploads() {
const context = react.useContext(UnfinishedUploadsContext);
if (!context) {
throw new Error("useUnfinishedUploads must be used within UploadzxProvider");
}
return context;
}
function useUploadzxContext() {
const actions = useUploadzxActions();
const state = useUploadzxState();
const { uploadStates } = useUploadStates();
const { queueStats } = useQueueStats();
const { unfinishedUploads } = useUnfinishedUploads();
return {
...actions,
...state,
uploadStates,
queueStats,
unfinishedUploads
};
}
// src/react/hooks/useUploadState.ts
function useUploadState(fileId) {
const { uploadStates } = useUploadStates();
const [state, setState] = react.useState(null);
react.useEffect(() => {
try {
const state2 = uploadStates?.[fileId] || null;
setState(state2);
} catch (error) {
console.error("Error getting upload state for fileId:", fileId, error);
}
}, [uploadStates, fileId]);
return state;
}
function useUploadProgress(uploadStates, fileId) {
const [progress, setProgress] = react.useState(null);
react.useEffect(() => {
const state = uploadStates[fileId];
if (state?.progress) {
setProgress(state.progress);
} else {
setProgress(null);
}
}, [uploadStates, fileId]);
return progress;
}
function useFilePicker(options = {}) {
const filePicker = new FilePicker(options);
const pickFiles = react.useCallback(async () => {
return await filePicker.pickFiles();
}, [filePicker]);
return {
pickFiles
};
}
function useUploadItem(fileId) {
const state = useUploadState(fileId);
const { pauseUpload, resumeUpload, cancelUpload } = useUploadzxActions();
const handlePause = react.useCallback(() => {
pauseUpload(fileId);
}, [pauseUpload, fileId]);
const handleResume = react.useCallback(() => {
resumeUpload(fileId);
}, [resumeUpload, fileId]);
const handleCancel = react.useCallback(() => {
cancelUpload(fileId);
}, [cancelUpload, fileId]);
const canPause = react.useMemo(() => {
if (!state) return false;
return state.status === "uploading";
}, [state?.status]);
const canResume = react.useMemo(() => {
if (!state) return false;
return state.status === "paused";
}, [state?.status]);
const canCancel = react.useMemo(() => {
if (!state) return false;
return state.status !== "completed" && state.status !== "cancelled";
}, [state?.status]);
const progress = react.useMemo(() => state?.progress, [state?.progress?.bytesUploaded]);
return {
handlePause,
handleResume,
handleCancel,
canPause,
canResume,
canCancel,
progress
};
}
function useQueueActions() {
const { pauseAll, resumeAll, cancelAll, pickAndUploadFiles } = useUploadzxActions();
const { queueStats } = useQueueStats();
const handlePauseAll = react.useCallback(() => {
pauseAll();
}, [pauseAll]);
const handleResumeAll = react.useCallback(() => {
resumeAll();
}, [resumeAll]);
const handleCancelAll = react.useCallback(() => {
cancelAll();
}, [cancelAll]);
const handlePickFiles = react.useCallback(() => {
pickAndUploadFiles();
}, [pickAndUploadFiles]);
const canPauseAll = react.useMemo(() => queueStats.activeCount > 0, [queueStats.activeCount]);
const hasActiveUploads = react.useMemo(() => queueStats.activeCount > 0, [queueStats.activeCount]);
const hasQueuedUploads = react.useMemo(() => queueStats.queueLength > 0, [queueStats.queueLength]);
const queueStatsText = react.useMemo(
() => `${queueStats.queueLength} in queue, ${queueStats.activeCount} active`,
[queueStats.queueLength, queueStats.activeCount]
);
return {
handlePauseAll,
handleResumeAll,
handleCancelAll,
handlePickFiles,
canPauseAll,
hasActiveUploads,
hasQueuedUploads,
queueStatsText,
queueStats
};
}
function UploadDropzone({
children,
onFilesDrop,
className = "",
activeClassName = "",
disabled = false,
clickable = false
}) {
const [isDragActive, setIsDragActive] = react.useState(false);
const { addFiles, pickAndUploadFiles } = useUploadzxActions();
const handleDragEnter = react.useCallback(
(e) => {
e.preventDefault();
e.stopPropagation();
if (!disabled) {
setIsDragActive(true);
}
},
[disabled]
);
const handleDragLeave = react.useCallback((e) => {
e.preventDefault();
e.stopPropagation();
setIsDragActive(false);
}, []);
const handleDragOver = react.useCallback((e) => {
e.preventDefault();
e.stopPropagation();
setIsDragActive(true);
}, []);
const handleDrop = react.useCallback(
async (e) => {
e.preventDefault();
e.stopPropagation();
setIsDragActive(false);
if (disabled) return;
try {
const fileHandlePairs = await getFilesFromDragEvent(e);
if (fileHandlePairs.length > 0) {
const uploadFiles = fileHandlePairs.map(({ file, handle }) => ({
id: generateFileId(),
file,
fileHandle: handle,
name: file.name,
size: file.size,
type: file.type
}));
console.log(`Dropped ${uploadFiles.length} files with handles:`, uploadFiles);
onFilesDrop?.(uploadFiles);
await addFiles(uploadFiles);
}
} catch (error) {
console.error("Error processing dropped files:", error);
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
const uploadFiles = files.map((file) => ({
id: generateFileId(),
file,
name: file.name,
size: file.size,
type: file.type
}));
onFilesDrop?.(uploadFiles);
await addFiles(uploadFiles);
}
}