UNPKG

@masvio/downloader

Version:

A simple, lightweight library to easily download files from MASV links

1,277 lines (1,276 loc) 38.8 kB
class b { constructor(e) { if (this.ma = 0, this.previousTime = 0, e <= 0) throw new Error("must provide a timespan > 0 to the moving average constructor"); this.timespan = e; } alpha(e, t) { return 1 - Math.exp(-(e - t) / this.timespan); } push(e, t) { if (this.previousTime > 0) { const r = this.alpha(e, this.previousTime); this.ma = r * t + (1 - r) * this.ma; } else this.ma = t; this.previousTime = e; } // Exponential Moving Average movingAverage() { return this.ma; } } var f = /* @__PURE__ */ ((n) => (n.Start = "download_start", n.Stop = "download_stop", n.Pause = "download_pause", n.Finish = "download_finish", n.Continue = "download_continue", n))(f || {}); const k = 500; class M { constructor() { this.total = 0, this.marks = { download_start: [], download_pause: [], download_continue: [], download_finish: [], download_stop: [] }, this.moving = new b(k), this.debounceAverage = !1, this.lastSampleAmount = 0, this.progress = 0, this.chunkProgress = 0, this.fileProgress = 0, this.instantSpeed = 0, this.movingSpeed = 0, this.totalFiles = 0, this.finalizedFiles = 0, this.duration = 0, this.timeOffset = 0, this.stopped = !0; } reset() { this.total = 0, this.marks = { download_start: [], download_pause: [], download_continue: [], download_finish: [], download_stop: [] }, this.moving = new b(k), this.debounceAverage = !1, this.lastSampleAmount = 0, this.progress = 0, this.chunkProgress = 0, this.fileProgress = 0, this.instantSpeed = 0, this.movingSpeed = 0, this.duration = 0, this.timeOffset = 0, this.stopped = !0, this.totalFiles = 0, this.finalizedFiles = 0; } mark(e) { this.marks[e] && (e === "download_start" || e === "download_continue" ? (this.stopped = !1, this.record(performance.now() - this.timeOffset)) : (this.stopped = !0, this.timeOffset = this.duration), this.marks[e].push(Date.now())); } record(e = performance.now()) { requestAnimationFrame((t) => { this.stopped || (this.duration = t - e, this.record(e)); }); } setTotal(e) { this.total = e; } setTotalFiles(e) { this.totalFiles = e; } addProgress(e = 0) { const t = Date.now(); if (this.progress += e, this.debounceAverage) return; this.debounceAverage = !0; const r = this.progress - this.lastSampleAmount; this.instantSpeed = 8 * r / (k / 1e3), this.lastSampleAmount = this.progress, this.moving.push(t, this.instantSpeed), this.movingSpeed = this.moving.movingAverage(), setTimeout(() => { this.debounceAverage = !1; }, k); } addFileProgress(e = 0) { this.fileProgress += e, this.finalizedFiles++; } addChunkProgress(e = 0) { this.chunkProgress += e; } subtractProgress(e = 0) { this.progress -= e; } getStats() { return { duration: this.duration, speed: this.averageSpeed, instant: this.instantSpeed, moving: this.movingSpeed, total: this.total, progress: this.progress, chunkProgress: this.chunkProgress, fileProgress: this.fileProgress, totalFiles: this.totalFiles, finalizedFiles: this.finalizedFiles }; } // Measured in bps (bits per second) get averageSpeed() { return 8 * this.progress / (this.duration / 1e3); } } var o = /* @__PURE__ */ ((n) => (n.Start = "download:start", n.Progress = "download:progress", n.Chunk = "download:chunk", n.File = "download:file", n.Error = "download:error", n.Finished = "download:finish", n.FileQueued = "download:file_queued", n.FileDownloaded = "download:file_downloaded", n.Abort = "download:abort", n.Retry = "download:retry", n.PartialFinished = "download:partial_finish", n.FileErrored = "download:file_errored", n))(o || {}), l = /* @__PURE__ */ ((n) => (n.Created = "worker:create", n.Cancelled = "worker:cancelled", n.Execute = "worker:execute", n.Finish = "worker:finish", n.FinishFile = "worker:finish-file", n.FinishChunk = "worker:finish-chunk", n.LinkExpired = "worker:link-expired", n.Error = "worker:error", n.Retry = "worker:retry", n.Progress = "worker:progress", n.Terminate = "worker:terminate", n.Abort = "worker:abort", n.CreateParentDirectory = "worker:create-parent-directory", n))(l || {}); class F { constructor() { this.handlers = {}; } on(e, t) { this.handlers[e] || (this.handlers[e] = []), this.handlers[e].push(t); } off(e, t) { if (t == null) { this.handlers[e] = []; return; } this.handlers[e] && (this.handlers[e] = this.handlers[e].filter( (r) => r !== t )); } clearHandlers() { this.handlers = {}; } emit(e, t) { const r = { time: Date.now(), event: e, target: this, data: {}, ...t }; this.handlers[e] && this.handlers[e].forEach((s) => { s(r); }), this.handlers.emit != null && this.handlers.emit.forEach((s) => { s(r); }); } bubbleEmit(e) { this.emit(e.event, { ...e }); } } var g = /* @__PURE__ */ ((n) => (n.File = "file", n.Directory = "directory", n.Metadata = "metadata", n.ZipMac = "zip_mac", n.ZipWindows = "zip_windows", n))(g || {}); const u = class u { constructor(e, t) { this.apiURL = e, this.masvUserAgent = t; } async request({ url: e, method: t = "GET", headers: r, version: s = "v1", body: i }, a = 0) { return new Promise((h, p) => { const c = new XMLHttpRequest(); if (c.open(t, [this.apiURL, s, e].join("/"), !0), c.setRequestHeader("Accept", "application/json"), c.setRequestHeader("Content-Type", "application/json"), c.setRequestHeader("Masv-User-Agent", this.masvUserAgent), r) for (const d in r) r[d] && c.setRequestHeader(d, r[d]); c.onload = () => { if (this.shouldRetry(c.status)) { if (a > u.MAX_RETRIES) { p(new Error(`Request failed after ${a - 1} retry attempts.`)); return; } const d = Math.pow(u.BACKOFF_FACTOR, a) * u.BASE_TIMEOUT; setTimeout(() => { console.warn("Retrying request..."), this.request({ url: e, method: t, headers: r, version: s, body: i }, a + 1).then((m) => h(m)).catch((m) => p(m)); }, d); } else try { let d; c.responseText && (d = JSON.parse(c.responseText)), h(d); } catch (d) { p(d); } }, c.onerror = () => { p(new Error(`Request failed with status ${c.status}`)); }, c.onabort = () => { p(new Error("Request aborted")); }, c.send(i); }); } fetchLinkDetails(e, t, r) { let s; return r && (s = { "X-Link-Password": r }), this.request({ url: `links/${e}?secret=${t}`, headers: s }); } fetchPackageFiles(e, t) { return this.request({ url: `packages/${e}/files`, headers: { "X-Package-Token": t } }); } fetchDownloadLink(e, t, r) { return this.request({ url: `packages/${e}/files/${r}/download`, headers: { "X-Package-Token": t } }); } async fetchDownloadLinks(e, t, r) { const s = new URLSearchParams({ file_ids: r.join(",") }).toString(); return (await this.request({ url: `packages/${e}/files/batch/download?${s}`, headers: { "X-Package-Token": t }, version: "v1.1" })).records; } notifyLinkDownloaded(e, t, r) { let s; r && (s = { "X-Link-Password": r }); const i = JSON.stringify({ secret: t }); return this.request({ url: `links/${e}/complete`, method: "PUT", headers: s, body: i }); } notifyError(e, t, r) { const s = { "X-Package-Token": t }, i = JSON.stringify(r); return this.request({ url: `packages/${e}/error`, method: "POST", headers: s, body: i }); } shouldRetry(e) { return [-1, 0].includes(e) || e >= 500; } }; u.MAX_RETRIES = 3, u.BASE_TIMEOUT = 1e3, u.BACKOFF_FACTOR = 2; let y = u; const I = `self.isAbort = !1; self.abortController = new AbortController(); self.abortSignal = self.abortController.signal; self.fileWriteStreams = {}; self.onmessage = async ({ data: t }) => { let { message: e, payload: s } = t; const r = { QueueFile: async () => await F({ payload: s }), Abort: async () => await D({ payload: s }), CreateParentDirectory: async () => await k({ payload: s }) }; r[e] && await r[e]({ data: t }); }; async function w(t, e) { try { const s = await e.getDirectoryHandle( t.shift(), { create: !0 } ); return t.length ? await w(t, s) : s; } catch (s) { throw s; } } async function m(t, e, s) { try { return await (await y( t, e, s )).getFileHandle(t.name, { create: !0 }); } catch (r) { throw r; } } async function y({ path: t }, e, s) { try { let r; if (t != null) { const o = t.split("/"); s && o.unshift(s), r = await w(o, e); } else s ? r = await w([s], e) : r = e; return r; } catch (r) { throw r; } } async function p(t, e) { let s = t; try { let r = 0; for (; r <= 9; ) await e.getDirectoryHandle(s), r++, s = \`\${t} (\${r})\`; return null; } catch (r) { if (r.name === "NotFoundError") return s; throw r; } } async function k({ payload: t }) { try { const { directoryHandle: e, packageName: s, packageId: r } = t; let o = await p( s, e ); if (!o) { const a = \`\${r} - \${crypto.randomUUID()}\`; o = await p(a, e); } if (!o) { console.error("Error when creating parent directory."); const a = new Error("Error when creating parent directory."); self.postMessage({ event: "worker:error", eventPayload: { error: a } }); return; } await e.getDirectoryHandle(o, { create: !0 }), self.postMessage({ event: "worker:create-parent-directory", eventPayload: { parentDirectoryName: o } }); } catch (e) { console.error( \`Error with download worker when creating parent directory: \${e}\` ), self.postMessage({ event: "worker:error", eventPayload: { error: e } }); } } async function v({ file: t, chunkIndex: e, start: s, end: r }, o = 0) { return new Promise(async (a, l) => { if (self.isAbort) { self.postMessage({ event: "worker:abort", eventPayload: { fileId: t.id, chunkIndex: e, message: \`Download successfully aborted for file: \${t.id}\` } }); return; } try { const n = await fetch(t.url, { keepalive: !0, headers: { range: \`bytes=\${s}-\${r}\` }, signal: self.abortSignal }); if (M(n.status, o)) { const i = new Error(\`Request failed with status: \${n.status}\`); i.status = n.status, l(i); } else if (n.status === 200 || n.status === 206) { const i = r - s + 1; await T( n, self.fileWriteStreams[t.id], s, i, { fileId: t.id, chunkIndex: e } ), a(); } else { const i = new Error( \`Unexpected response status code: \${n.status}\` ); i.status = n.status, l(i); } } catch (n) { l(n); } }).catch((a) => { if (self.isAbort) { self.postMessage({ event: "worker:abort", eventPayload: { fileId: t.id, chunkIndex: e, message: \`Download successfully aborted for file: \${t.id}\` } }); return; } if (a instanceof TypeError && a.message.includes("Failed to fetch")) throw a; if (!C(a)) throw console.error( \`Error cannot be retried for chunk at range \${s}-\${r}: \${a.message}\` ), a; const l = Math.pow(2, o) * 1e3; if (l > 9e4) throw console.error( \`Max retries reached for chunk at range \${s}-\${r}: \${a.message}\` ), a; return console.error(\`Error: \${a.message}. Retry Count: \${o}\`), self.postMessage({ event: "worker:retry", eventPayload: { fileId: t.id, chunkIndex: e, error: a } }), new Promise((n) => { setTimeout(() => { n( v({ file: t, chunkIndex: e, start: s, end: r }, o + 1) ); }, l); }); }); } function M(t) { return !(t >= 200 && t < 300); } function C(t) { const e = t.message, s = t.status, r = [-1, 0]; return s >= 500 || r.includes(s) || t instanceof TypeError && e.includes("network error"); } async function F({ payload: t }) { let { file: e, directoryHandle: s, packageName: r, chunkSize: o, retryAttempt: a, finishedChunks: l, chunkConcurrency: n } = t; try { if (P(), e.kind === "directory") { await (await y( e, s, r )).getDirectoryHandle(e.name, { create: !0 }), self.postMessage({ event: "worker:finish-file", eventPayload: { fileId: e.id, fileSize: 0 } }); return; } if (!e.size) { self.postMessage({ event: "worker:finish-file", eventPayload: { fileId: e.id, fileSize: 0 } }); return; } if (e.virus_detected) { self.postMessage({ event: "worker:finish-file", eventPayload: { fileId: e.id, fileSize: e.size, error: new Error( \`File with id \${e.id} contains a virus, download prevented.\` ) } }); return; } const i = await m( e, s, r ); if (!self.fileWriteStreams[e.id]) { const f = await i.createWritable(); self.fileWriteStreams = { [e.id]: f, ...self.fileWriteStreams }; } const c = Math.ceil(e.size / o); if (c) { let f = new Array(c).fill("").map((u, d) => async () => { const g = d * o, A = Math.min(e.size, g + o - 1); return v({ file: e, chunkIndex: d, start: g, end: A }).then(() => f.length > 0 ? f.shift()() : Promise.resolve()); }).filter((u, d) => !l.has(d)); const b = n || 6, E = Math.min(f.length, b), h = []; for (let u = 0; u < E; u++) { const d = f.shift(); h.push(d()); } await Promise.all(h); } if (self.isAbort) return; await self.fileWriteStreams[e.id].close(), delete self.fileWriteStreams[e.id], self.postMessage({ event: "worker:finish-file", eventPayload: { fileId: e.id, fileSize: e.size } }); } catch (i) { if (i instanceof TypeError && i.message.includes("Failed to fetch") && a <= 2) { self.postMessage({ event: "worker:link-expired", eventPayload: { fileId: e.id, retryAttempt: a } }); return; } try { self.fileWriteStreams[e.id] && (await self.fileWriteStreams[e.id].close(), await (await m( e, s, r )).remove()); } catch { console.error("Error with removing errored file handle."); } self.postMessage({ event: "worker:finish-file", eventPayload: { fileId: e.id, fileSize: e.size, error: i } }); } } async function D({ payload: t }) { const { requestType: e, deleteFiles: s, directoryHandle: r, packageName: o } = t; if (self.isAbort = !0, self.abortController.abort(), e === "Cancel") { try { for (const a in self.fileWriteStreams) await self.fileWriteStreams[a].close(); s && await (await y( { path: null }, r, o )).remove({ recursive: !0 }); } catch { return; } self.postMessage({ event: "worker:cancelled", eventPayload: {} }); } } async function T(t, e, s = 0, r, o = {}) { const a = t.body.getReader({ mode: "byob" }); let l = new ArrayBuffer(r), n = 0; try { for (; ; ) { const { done: i, value: c } = await a.read( new Uint8Array(l, n, l.byteLength - n) ); if (i) break; if (self.postMessage({ event: "worker:progress", eventPayload: { ...o, transferred: c.byteLength } }), l = c.buffer, n += c.byteLength, n >= r - 1) { self.postMessage({ event: "worker:finish-chunk", eventPayload: { ...o, chunkSize: r } }), await e.write({ type: "write", data: l, position: s, size: r }); break; } } } catch (i) { throw i.name !== "AbortError" && console.error("Error with read stream", i), i; } } function P() { self.isAbort && (self.isAbort = !1, self.abortController = new AbortController(), self.abortSignal = self.abortController.signal); } `, P = typeof self < "u" && self.Blob && new Blob(["URL.revokeObjectURL(import.meta.url);", I], { type: "text/javascript;charset=utf-8" }); function L(n) { let e; try { if (e = P && (self.URL || self.webkitURL).createObjectURL(P), !e) throw ""; const t = new Worker(e, { type: "module", name: n?.name }); return t.addEventListener("error", () => { (self.URL || self.webkitURL).revokeObjectURL(e); }), t; } catch { return new Worker( "data:text/javascript;charset=utf-8," + encodeURIComponent(I), { type: "module", name: n?.name } ); } } var S = /* @__PURE__ */ ((n) => (n.Cancel = "Cancel", n.Pause = "Pause", n))(S || {}); class T extends F { constructor() { super(), this.terminated = !1, this.onNativeWorkerMessage = ({ data: e }) => { const { event: t, eventPayload: r } = e; this.emit(t, { event: t, data: r }); }, this.emit(l.Created), this.startWorker(); } startWorker() { this.nativeWorker = new L(), this.nativeWorker.onmessage = this.onNativeWorkerMessage; } abort(e) { const t = { message: "Abort", payload: e }; this.nativeWorker.postMessage(t); } createParentDirectory(e) { const t = { message: "CreateParentDirectory", payload: e }; this.nativeWorker.postMessage(t); } terminate() { this.terminated = !0, this.emit(l.Terminate), this.clearHandlers(), this.nativeWorker.terminate(); } queueFile(e) { if (this.terminated) throw new Error("Can't use a terminated worker"); this.emit(l.Execute, { event: l.Execute, data: { requestPayload: e } }); const t = { message: "QueueFile", payload: e }; this.nativeWorker.postMessage(t); } } class A extends F { constructor(e, t, r, s = 6, i = 6) { super(), this.packageName = e, this.packageId = t, this.chunkSize = r, this.poolSize = s, this.chunkConcurrency = i, this.workers = new Array(), this.terminated = !1; let a; for (let h = 0; h < this.poolSize; h++) a = new T(), a.on("emit", this.bubbleEmit.bind(this)), this.workers.push(a); } prepareDownload(e, t, r) { for (const i of this.workers) i.startWorker(); if (r) return; const s = { directoryHandle: e, packageName: t, packageId: this.packageId }; this.workers[0].createParentDirectory(s); } queueFile(e, t, r, s, i = 0) { if (this.terminated) throw new v(); const a = this.workers[e.index % this.workers.length], h = { file: e.file, directoryHandle: r, packageName: s, chunkSize: this.chunkSize, retryAttempt: i, finishedChunks: t, chunkConcurrency: this.chunkConcurrency }; a.queueFile(h); } abort(e, t, r, s) { const i = { requestType: e, directoryHandle: t, packageName: r, deleteFiles: s }; this.workers.forEach((a) => { a.abort(i); }); } terminate() { this.terminated = !0, this.workers.forEach((e) => e.terminate()), this.clearHandlers(); } get workerPoolSize() { return this.poolSize; } } class z { constructor() { this.queue = {}; } getFile(e) { return this.queue[e]; } addFile(e) { const t = new x(e); return this.queue[e.fileId] = t, t; } removeFile(e) { delete this.queue[e]; } setProgress(e, t, r) { const s = this.getFile(e); s && s.addChunkProgress(t, r); } get length() { return Object.keys(this.queue).length; } } class x { constructor({ fileId: e, index: t, file: r, chunkSize: s }) { this.chunkProgress = {}, this.finishedChunks = /* @__PURE__ */ new Set(), this.id = e, this.file = r, this.index = t, r.size ? this.chunkSize = s > r.size ? r.size : s : this.chunkSize = s; } get totalProgress() { return Object.values(this.chunkProgress).reduce((e, t) => e + t, 0); } getChunkProgress(e) { return this.chunkProgress[e]; } clearChunkProgress(e) { this.chunkProgress[e] = 0; } writeChunk(e) { this.finishedChunks.add(e); } addChunkProgress(e, t) { e in this.chunkProgress || (this.chunkProgress[e] = 0), this.chunkProgress[e] += t; } } const R = 100 * 1024 * 1024, q = 6, _ = 6, N = 100, C = 300, O = 200, $ = 100; var D = /* @__PURE__ */ ((n) => (n.Idle = "idle", n.Downloading = "downloading", n.Ready = "ready", n.Paused = "paused", n.Finished = "finished", n.PartialFinished = "partial-finished", n.Errored = "errored", n.Terminated = "terminated", n.Cancelled = "cancelled", n))(D || {}); const w = class w extends F { constructor(e, t = "", r = "https://api.massive.app", s = R, i = "web-downloader") { super(), this.linkPassword = t, this.chunkSize = s, this.packageID = "", this.packageToken = "", this.packageName = "", this.packageSize = 0, this.directoryHandle = null, this.fileQueue = [], this.fileCount = 0, this.progressTracker = new M(), this.filesFinished = 0, this.currentFileIndex = 0, this.currentFetchedFileIndex = 0, this.fileMap = {}, this.pendingLinkQueue = [], this.finishedFiles = [], this.requestingLinks = !1, this.erroredFiles = [], this.fileConcurrency = this.getFileConcurrency(), this.chunkConcurrency = this.getChunkConcurrency(), this.status = "idle", this.downloadState = new z(), this.downloadCompleted = !1; try { const a = new URL(e); if (this.linkID = a.pathname.slice(1), this.linkSecret = a.searchParams.get("secret") ?? "", this.apiClient = new y(r, i), !this.linkSecret || !this.linkID) throw new Error("Invalid download link"); } catch (a) { throw console.error(a), this.cancel(), this.terminate(), this.changeStatus( "errored" /* Errored */ ), this.emit(o.Error, { event: o.Error, data: { performanceStats: this.performanceStats, error: a } }), a; } } async initialize() { try { await this.getPackageInfo(), await this.loadFiles(); const e = this.packageSize / this.fileCount; this.chunkConcurrency = this.getChunkConcurrency(e), this.fileConcurrency = this.getFileConcurrency(e), this.fdownloader = new A( this.packageName, this.packageID, this.chunkSize, this.fileConcurrency, this.chunkConcurrency ), this.fdownloader.on( l.FinishFile, this.onFileFinish.bind(this) ), this.fdownloader.on( l.FinishChunk, this.onChunkFinish.bind(this) ), this.fdownloader.on( l.Progress, this.onFileProgress.bind(this) ), this.fdownloader.on(l.Error, this.onDownloadError.bind(this)), this.fdownloader.on(l.Retry, this.onDownloadRetry.bind(this)), this.fdownloader.on(l.Abort, this.onDownloadAbort.bind(this)), this.fdownloader.on( l.CreateParentDirectory, this.onCreateParentDirectory.bind(this) ), this.fdownloader.on( l.Cancelled, this.onDownloadCancelled.bind(this) ), this.fdownloader.on( l.LinkExpired, this.onDownloadLinkExpired.bind(this) ), this.fdownloader.on("emit", this.bubbleEmit.bind(this)); } catch (e) { console.error(e), this.cancel(), this.terminate(), this.changeStatus( "errored" /* Errored */ ), this.emit(o.Error, { event: o.Error, data: { performanceStats: this.performanceStats, error: e } }); } } changeStatus(e) { this.status = e, this.emit(this.status); } async onCreateParentDirectory(e) { const { data: t } = e; this.parentDirectoryName = t.parentDirectoryName, await this.startDownload(); } onDownloadCancelled(e) { this.changeStatus( "cancelled" /* Cancelled */ ); } async onFileFinish(e) { try { const { data: t } = e, { fileId: r, fileSize: s, error: i } = t; this.filesFinished++; const a = this.downloadState.getFile(r); if (i ? (console.error(`Error downloading file ${r}: ${i}`), a?.totalProgress && this.progressTracker.subtractProgress(a?.totalProgress), this.erroredFiles.push(this.fileMap[r]), this.emit(o.FileErrored, { event: o.FileErrored, data: { fileId: r, error: i } })) : (this.progressTracker.addFileProgress(s), this.emit(o.FileDownloaded, { event: o.FileDownloaded, data: { fileId: r } })), a && this.downloadState.removeFile(a.id), this.filesFinished != this.fileCount) { await this.processQueue(); return; } if (this.downloadCompleted = !0, this.progressTracker.mark(f.Finish), this.erroredFiles.length) { this.emit(o.PartialFinished, { event: o.PartialFinished, data: { performanceStats: this.performanceStats, failedFiles: this.erroredFiles } }), this.changeStatus( "partial-finished" /* PartialFinished */ ); const h = { error_code: "partial_download", error_message: "Some files failed to be downloaded", event_time: (/* @__PURE__ */ new Date()).toISOString(), extras: this.progressSummary }; this.apiClient.notifyError(this.packageID, this.packageToken, h); return; } this.emit(o.Finished, { event: o.Finished, data: { performanceStats: this.performanceStats } }), this.changeStatus( "finished" /* Finished */ ), await this.apiClient.notifyLinkDownloaded(this.linkID, this.linkSecret, this.linkPassword).catch((h) => console.error(h)), this.terminate(); } catch (t) { console.error(t), this.cancel(), this.terminate(), this.changeStatus( "errored" /* Errored */ ), this.emit(o.Error, { event: o.Error, data: { performanceStats: this.performanceStats, error: t } }); } } onChunkFinish(e) { const { data: t } = e, { fileId: r, chunkIndex: s, chunkSize: i } = t, a = this.downloadState.getFile(r); a && (this.finishedFiles.push(a.file.name), a.writeChunk(s), this.progressTracker.addChunkProgress(i)); } onFileProgress(e) { const { data: t } = e, { chunkIndex: r, transferred: s, fileId: i } = t; this.downloadState.setProgress(i, r, s), this.progressTracker.addProgress(s), this.emit(o.Progress, { event: o.Progress, data: { performanceStats: this.performanceStats } }); } onDownloadError(e) { const { data: t } = e, { error: r } = t; this.changeStatus( "errored" /* Errored */ ), this.emit(o.Error, { event: o.Error, data: { performanceStats: this.performanceStats, error: r } }), console.error(r), this.cancel(), this.terminate(); } onDownloadRetry(e) { const { data: t } = e, { error: r } = t; this.emit(o.Retry, { event: o.Retry, data: { performanceStats: this.performanceStats, error: r } }); } onDownloadAbort(e) { const { data: t } = e, { fileId: r, chunkIndex: s } = t, i = this.downloadState.getFile(r); if (!i) return; const a = i.getChunkProgress(s); this.progressTracker.subtractProgress(a), i.clearChunkProgress(s), this.emit(o.Abort, { event: o.Abort, data: { fileId: i.id } }); } async onDownloadLinkExpired(e) { try { const { data: t } = e, { fileId: r, retryAttempt: s } = t, i = await this.apiClient.fetchDownloadLink( this.packageID, this.packageToken, r ), a = this.downloadState.getFile(r); if (!a) return; a.file.url = i.url, await this.processQueue(a, s + 1); } catch (t) { console.error(t), this.cancel(), this.terminate(), this.changeStatus( "errored" /* Errored */ ), this.emit(o.Error, { event: o.Error, data: { performanceStats: this.performanceStats, error: t } }); } } get canDownloadFile() { return this.status === "ready" || this.status === "downloading"; } get allDownloadLinksFetched() { return this.currentFetchedFileIndex >= Object.values(this.fileMap).length; } isLowFileDensity(e) { return e <= 0.1 * this.chunkSize; } isHighFileDensity(e) { return e >= 10 * this.chunkSize; } getFileConcurrency(e) { return e ? this.isLowFileDensity(e) ? 12 : this.isHighFileDensity(e) ? 1 : 6 : q; } getChunkConcurrency(e) { return e ? this.isLowFileDensity(e) ? 1 : this.isHighFileDensity(e) ? 6 : 1 : _; } async fetchMetaDataLinks() { const t = Object.values(this.fileMap).filter((r) => r.kind === g.Metadata).map((r) => r.id); if (t.length) for (const r of t) { const s = await this.apiClient.fetchDownloadLink( this.packageID, this.packageToken, r ); this.pendingLinkQueue.push({ fileId: r, url: s.url }); } } chunkFile(e, t) { const r = this.downloadState.getFile(e.id), s = Math.min(t, e.size); return r ? { fileId: e.id, index: r.index, file: e, chunkSize: s } : { fileId: e.id, index: this.currentFileIndex, file: e, chunkSize: s }; } // If given file state, process the given file state // Otherwise, process the next file in the queue async processQueue(e, t = 0) { if (!this.canDownloadFile) return; if (!this.directoryHandle) throw new Error("No directory provided."); const r = e ? e.file : this.fileQueue.pop(); if (!r) return; const s = this.chunkFile(r, this.chunkSize); this.emit(o.FileQueued, { event: o.FileQueued, data: { fileId: r.id } }); const i = e ? e.finishedChunks : /* @__PURE__ */ new Set(); this.fdownloader.queueFile( s, i, this.directoryHandle, this.parentDirectoryName, t ), e || (this.downloadState.addFile(s), this.currentFileIndex++), await this.fillFileQueue(); } queueEmptyFilesAndDirectories() { const e = Object.values(this.fileMap).filter( (t) => t.kind === g.Directory || t.kind === g.File && !t.size ); this.fileQueue.push(...e); } async fetchDownloadLinks(e) { if (this.requestingLinks || (this.requestingLinks = !0, this.pendingLinkQueue.length >= C)) return; const t = Math.min(Object.values(this.fileMap).length, e), r = Object.values(this.fileMap); let s = this.currentFetchedFileIndex + t; s > r.length && (s = r.length); const i = r.filter((h) => h.kind === g.File && h.size).slice(this.currentFetchedFileIndex, s).map((h) => h.id); if (this.currentFetchedFileIndex += e, !i.length) return; const a = await this.apiClient.fetchDownloadLinks( this.packageID, this.packageToken, i ); for (const h of a) this.pendingLinkQueue.push({ fileId: h.id, url: h.url }); this.requestingLinks = !1; } async fillFileQueue() { for (; this.fileQueue.length < $; ) { const e = this.pendingLinkQueue.pop(); if (!e) return; const t = { ...this.fileMap[e.fileId], url: e.url }; this.fileQueue.push(t); } !this.allDownloadLinksFetched && this.pendingLinkQueue.length < O && await this.fetchDownloadLinks(N); } async prepareDownload(e, t) { this.filesFinished = 0, this.currentFileIndex = 0, this.currentFetchedFileIndex = 0, this.downloadCompleted = !1, this.progressTracker.reset(), this.progressTracker.setTotal(this.totalSize), this.progressTracker.setTotalFiles(this.fileCount), this.erroredFiles = [], this.queueEmptyFilesAndDirectories(), await this.fetchMetaDataLinks(), await this.fetchDownloadLinks(C - this.pendingLinkQueue.length), await this.fillFileQueue(), this.directoryHandle = e, this.progressTracker.mark(f.Start), this.fdownloader.prepareDownload( e, this.parentDirectoryName, t ); } async getPackageInfo() { const e = await this.apiClient.fetchLinkDetails( this.linkID, this.linkSecret, this.linkPassword ); return this.packageID = e.package_id, this.packageName = e.name, this.packageToken = e.package_token, this.packageSize = e.package_size, e; } pause() { try { if (this.status === "paused" || !this.directoryHandle) return; this.changeStatus( "paused" /* Paused */ ), this.progressTracker.mark(f.Pause), this.fdownloader.abort( S.Pause, this.directoryHandle, this.parentDirectoryName, !1 ); } catch (e) { console.error(e), this.cancel(), this.terminate(), this.changeStatus( "errored" /* Errored */ ), this.emit(o.Error, { event: o.Error, data: { performanceStats: this.performanceStats, error: e } }); } } cancel(e = !1) { try { if (this.status === "cancelled" || !this.directoryHandle) return; this.changeStatus( "cancelled" /* Cancelled */ ), this.progressTracker.mark(f.Pause), this.fdownloader.abort( S.Cancel, this.directoryHandle, this.parentDirectoryName, e ); } catch (t) { console.error(t), this.terminate(), this.changeStatus( "errored" /* Errored */ ), this.emit(o.Error, { event: o.Error, data: { performanceStats: this.performanceStats, error: t } }); } } stop() { this.progressTracker.mark(f.Stop), this.changeStatus( "idle" /* Idle */ ); } terminate() { try { if (this.status === "terminated") return; this.changeStatus( "terminated" /* Terminated */ ), this.clearHandlers(), this.fdownloader.terminate(); } catch (e) { this.changeStatus( "errored" /* Errored */ ), this.emit(o.Error, { event: o.Error, data: { performanceStats: this.performanceStats, error: e } }); } } async loadFiles() { try { let e = await this.apiClient.fetchPackageFiles( this.packageID, this.packageToken ); e || (e = []); const t = {}; for (const r of e) t[r.id] = r; return this.fileMap = t, this.fileCount = Object.values(this.fileMap).filter( (r) => !r.kind.includes("zip") ).length, e; } catch (e) { throw console.error(e), this.cancel(), this.terminate(), this.changeStatus( "errored" /* Errored */ ), this.emit(o.Error, { event: o.Error, data: { performanceStats: this.performanceStats, error: e } }), e; } } async retry() { try { if (this.status != "partial-finished") return; this.changeStatus( "downloading" /* Downloading */ ); const e = {}; for (const t of this.erroredFiles) e[t.id] = t; this.fileMap = e, this.fileCount = Object.values(this.fileMap).filter( (t) => !t.kind.includes("zip") ).length, await this.prepareDownload(this.directoryHandle, !0), await this.startDownload(); } catch (e) { console.error(e), this.cancel(), this.terminate(), this.changeStatus( "errored" /* Errored */ ), this.emit(o.Error, { event: o.Error, data: { performanceStats: this.performanceStats, error: e } }); } } async start(e, t) { try { if (this.status === "downloading") return; this.changeStatus( "downloading" /* Downloading */ ), this.parentDirectoryName || (this.parentDirectoryName = this.packageName); const { progress: r } = this.progressTracker.getStats(); if (r > 0) { this.progressTracker.mark(f.Continue); const s = Object.values(this.downloadState.queue); for (const a of s) await this.processQueue(a); const i = Math.min( this.fileConcurrency, Object.values(this.fileMap).length ); for (let a = 0; a < i - s.length; a++) await this.processQueue(); return; } if (t?.length) { const s = {}; for (const i of t) s[i.id] = i; this.fileMap = s, this.fileCount = Object.values(this.fileMap).filter( (i) => !i.kind.includes("zip") ).length; } await this.prepareDownload(e); } catch (r) { console.error(r), this.cancel(), this.terminate(), this.changeStatus( "errored" /* Errored */ ), this.emit(o.Error, { event: o.Error, data: { performanceStats: this.performanceStats, error: r } }); } } async startDownload() { const e = Math.min( this.fileConcurrency, Object.values(this.fileMap).length ); for (let t = 0; t < e; t++) await this.processQueue(); } get totalSize() { return Object.values(this.fileMap).reduce((t, r) => t + (r.size || 0), 0); } get performanceStats() { return this.progressTracker.getStats(); } get progressSummary() { const e = this.performanceStats; return { finalizedBytes: String(e.fileProgress), finalizedFiles: String(e.finalizedFiles), totalBytes: String(e.total), totalFiles: String(e.totalFiles), erroredFiles: String(this.erroredFiles.length) }; } }; w.DownloaderEvents = o, w.Emit = "emit", w.States = D; let E = w; class v extends Error { constructor() { super("Terminated"), Object.setPrototypeOf(this, v.prototype); } } export { E as Downloader, D as DownloaderStates, v as TerminatedError };