UNPKG

uploadzx

Version:

Browser-only TypeScript upload library with tus integration for resumable uploads

1,534 lines (1,525 loc) 51.2 kB
'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); } }