@masvio/downloader
Version:
A simple, lightweight library to easily download files from MASV links
1,277 lines (1,276 loc) • 38.8 kB
JavaScript
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
};