@masvio/downloader
Version:
A simple, lightweight library to easily download files from MASV links
585 lines (584 loc) • 39.5 kB
JavaScript
var MASV=function(w){"use strict";class L{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 n=this.alpha(e,this.previousTime);this.ma=n*t+(1-n)*this.ma}else this.ma=t;this.previousTime=e}movingAverage(){return this.ma}}var g=(r=>(r.Start="download_start",r.Stop="download_stop",r.Pause="download_pause",r.Finish="download_finish",r.Continue="download_continue",r))(g||{});const F=500;class H{constructor(){this.total=0,this.marks={download_start:[],download_pause:[],download_continue:[],download_finish:[],download_stop:[]},this.moving=new L(F),this.lastSampleAmount=0,this.lastSampleTime=Date.now(),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 L(F),this.lastSampleAmount=0,this.lastSampleTime=Date.now(),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.lastSampleTime=Date.now(),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){this.progress+=e;const t=Date.now();if(t-this.lastSampleTime<F&&this.progress>e)return;const n=Math.max(t-this.lastSampleTime,F),i=this.progress-this.lastSampleAmount;this.instantSpeed=8*i/(n/1e3),this.lastSampleAmount=this.progress,this.lastSampleTime=t,this.moving.push(t,this.instantSpeed),this.movingSpeed=this.moving.movingAverage()}addFileProgress(e=0){this.finalizedFiles++,this.fileProgress+=e}addChunkProgress(e=0){this.chunkProgress+=e}subtractProgress(e=0){this.progress-=e,this.lastSampleAmount=this.progress}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}}get averageSpeed(){return 8*this.progress/(this.duration/1e3)}}var o=(r=>(r.Start="download:start",r.Progress="download:progress",r.Chunk="download:chunk",r.File="download:file",r.Error="download:error",r.Finished="download:finish",r.FileQueued="download:file-queued",r.FileErrored="download:file-errored",r.FileDownloaded="download:file-downloaded",r.Abort="download:abort",r.Retry="download:retry",r.PartialFinished="download:partial-finish",r.ParentDirectoryCreated="download:parent-directory-created",r))(o||{}),c=(r=>(r.Created="worker:create",r.Cancelled="worker:cancelled",r.Execute="worker:execute",r.Finish="worker:finish",r.FinishFile="worker:finish-file",r.FinishChunk="worker:finish-chunk",r.LinkExpired="worker:link-expired",r.Error="worker:error",r.Retry="worker:retry",r.Progress="worker:progress",r.Terminate="worker:terminate",r.Abort="worker:abort",r.ParentDirectoryCreated="worker:parent-directory-created",r))(c||{});class P{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(n=>n!==t))}clearHandlers(){this.handlers={}}emit(e,t){const n={time:Date.now(),event:e,target:this,data:{},...t};this.handlers[e]&&this.handlers[e].forEach(i=>{i(n)}),this.handlers.emit!=null&&this.handlers.emit.forEach(i=>{i(n)})}bubbleEmit(e){this.emit(e.event,{...e})}}var m=(r=>(r.File="file",r.Directory="directory",r.Metadata="metadata",r.ZipMac="zip_mac",r.ZipWindows="zip_windows",r))(m||{});class u extends Error{constructor(e){super(e),this.name="AbortError"}}const f=class f{constructor(e,t){this.apiURL=e,this.masvUserAgent=t,this.requestList={}}async request({url:e,method:t="GET",headers:n,version:i="v1",body:s},a=0){return new Promise((h,y)=>{const p=crypto.randomUUID(),l=new XMLHttpRequest;if(l.open(t,[this.apiURL,i,e].join("/"),!0),l.setRequestHeader("Accept","application/json"),l.setRequestHeader("Content-Type","application/json"),l.setRequestHeader("Masv-User-Agent",this.masvUserAgent),n)for(const d in n)n[d]&&l.setRequestHeader(d,n[d]);const R=()=>{const d=Math.min(f.MAX_TIMEOUT,Math.pow(f.BACKOFF_FACTOR,a)*f.BASE_TIMEOUT);setTimeout(()=>{if(!(p in this.requestList)){y(new u("Request aborted"));return}this.request({url:e,method:t,headers:n,version:i,body:s},a+1).then(M=>h(M)).catch(M=>y(M))},d)};l.onload=()=>{if(this.shouldRetry(l.status))R();else{p in this.requestList&&delete this.requestList[p];try{let d;l.responseText&&(d=JSON.parse(l.responseText)),h(d)}catch(d){y(d)}}},l.onerror=()=>{this.shouldRetry(l.status)?R():(p in this.requestList&&delete this.requestList[p],y(new Error(`Request failed with status ${l.status}`)))},l.onabort=()=>{p in this.requestList&&delete this.requestList[p],y(new u("Request aborted"))},this.requestList[p]=l,l.send(s)})}fetchLinkDetails(e,t,n,i){let s;return n?s={"X-Link-Password":n}:i&&(s={"X-User-Token":i}),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,n){return this.request({url:`packages/${e}/files/${n}/download`,headers:{"X-Package-Token":t}})}async fetchDownloadLinks(e,t,n){const i=new URLSearchParams({file_ids:n.join(",")}).toString();return(await this.request({url:`packages/${e}/files/batch/download?${i}`,headers:{"X-Package-Token":t},version:"v1.1"})).records}notifyLinkDownloaded(e,t,n,i){let s;n?s={"X-Link-Password":n}:i&&(s={"X-User-Token":i});const a=JSON.stringify({secret:t});return this.request({url:`links/${e}/complete`,method:"PUT",headers:s,body:a})}notifyError(e,t,n){const i={"X-Package-Token":t},s=JSON.stringify(n);return this.request({url:`packages/${e}/error`,method:"POST",headers:i,body:s})}abortAll(){for(const e in this.requestList)this.requestList[e].abort(),delete this.requestList[e]}shouldRetry(e){return[-1,0].includes(e)||e>=500}};f.BASE_TIMEOUT=1e3,f.MAX_TIMEOUT=3e4,f.BACKOFF_FACTOR=2;let C=f;var b=(r=>(r.QueueFile="QueueFile",r.Abort="Abort",r.CreateParentDirectory="CreateParentDirectory",r))(b||{}),v=(r=>(r.Cancel="Cancel",r.Pause="Pause",r))(v||{});const I=`class f extends Error {
constructor(e) {
super(e), this.name = "AbortError";
}
}
const m = class m {
constructor(e, t) {
this.apiURL = e, this.masvUserAgent = t, this.requestList = {};
}
async request({ url: e, method: t = "GET", headers: r, version: s = "v1", body: a }, d = 0) {
return new Promise((p, u) => {
const o = crypto.randomUUID(), l = new XMLHttpRequest();
if (l.open(t, [this.apiURL, s, e].join("/"), !0), l.setRequestHeader("Accept", "application/json"), l.setRequestHeader("Content-Type", "application/json"), l.setRequestHeader("Masv-User-Agent", this.masvUserAgent), r)
for (const n in r)
r[n] && l.setRequestHeader(n, r[n]);
const c = () => {
const n = Math.min(
m.MAX_TIMEOUT,
Math.pow(m.BACKOFF_FACTOR, d) * m.BASE_TIMEOUT
);
setTimeout(() => {
if (!(o in this.requestList)) {
u(new f("Request aborted"));
return;
}
this.request({ url: e, method: t, headers: r, version: s, body: a }, d + 1).then((y) => p(y)).catch((y) => u(y));
}, n);
};
l.onload = () => {
if (this.shouldRetry(l.status))
c();
else {
o in this.requestList && delete this.requestList[o];
try {
let n;
l.responseText && (n = JSON.parse(l.responseText)), p(n);
} catch (n) {
u(n);
}
}
}, l.onerror = () => {
this.shouldRetry(l.status) ? c() : (o in this.requestList && delete this.requestList[o], u(new Error(\`Request failed with status \${l.status}\`)));
}, l.onabort = () => {
o in this.requestList && delete this.requestList[o], u(new f("Request aborted"));
}, this.requestList[o] = l, l.send(a);
});
}
fetchLinkDetails(e, t, r, s) {
let a;
return r ? a = { "X-Link-Password": r } : s && (a = { "X-User-Token": s }), this.request({
url: \`links/\${e}?secret=\${t}\`,
headers: a
});
}
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, s) {
let a;
r ? a = { "X-Link-Password": r } : s && (a = { "X-User-Token": s });
const d = JSON.stringify({ secret: t });
return this.request({
url: \`links/\${e}/complete\`,
method: "PUT",
headers: a,
body: d
});
}
notifyError(e, t, r) {
const s = { "X-Package-Token": t }, a = JSON.stringify(r);
return this.request({
url: \`packages/\${e}/error\`,
method: "POST",
headers: s,
body: a
});
}
abortAll() {
for (const e in this.requestList)
this.requestList[e].abort(), delete this.requestList[e];
}
shouldRetry(e) {
return [-1, 0].includes(e) || e >= 500;
}
};
m.BASE_TIMEOUT = 1e3, m.MAX_TIMEOUT = 3e4, m.BACKOFF_FACTOR = 2;
let E = m;
class T {
constructor(e, t) {
this.directoryHandle = e, this.packageName = t, this.fileWriteStreams = {}, this.filesWaitingForClose = {};
}
async createParentDirectory(e, t) {
let r = await this.getParentDirectoryName(e);
if (!r) {
const s = \`\${t} - \${crypto.randomUUID()}\`;
r = await this.getParentDirectoryName(s);
}
if (!r)
throw new Error("Error when creating parent directory.");
return this.packageName = r, await this.directoryHandle.getDirectoryHandle(r, {
create: !0
}), r;
}
async deleteParentDirectory() {
for (const s in this.fileWriteStreams)
await this.closeWritableFileHandle(s);
this.fileWriteStreams = {};
const e = async (s) => {
try {
return await this.directoryHandle.getDirectoryHandle(s), !0;
} catch {
return !1;
}
}, t = (s) => {
const a = Math.random() * 50;
return new Promise((d) => setTimeout(d, s + a));
}, r = 1e3;
for (let s = 0; s < r; s++)
try {
if (!await e(this.packageName))
break;
await this.directoryHandle.removeEntry(this.packageName, { recursive: !0 });
break;
} catch {
await t(1);
}
}
async createEmptyDirectory(e) {
await (await this.getDirectoryHandle(e)).getDirectoryHandle(e.name, { create: !0 });
}
async getFileHandle(e) {
try {
return await (await this.getDirectoryHandle(e)).getFileHandle(e.name, { create: !0 });
} catch (t) {
throw t;
}
}
async getWritableFileHandle(e, t) {
if (e.id in this.filesWaitingForClose)
throw new f("File is already closing");
if (e.id in this.fileWriteStreams)
return this.fileWriteStreams[e.id];
t || (t = await this.getFileHandle(e));
const r = await t.createWritable();
return this.fileWriteStreams[e.id] = r, r;
}
async closeWritableFileHandle(e) {
try {
if (!(e in this.fileWriteStreams))
return;
e in this.filesWaitingForClose || (this.filesWaitingForClose[e] = new Promise(async (t, r) => {
try {
const s = this.fileWriteStreams[e];
delete this.fileWriteStreams[e], await s.close(), t();
} catch {
t();
}
})), await this.filesWaitingForClose[e], delete this.filesWaitingForClose[e];
} catch (t) {
throw console.warn("Failed to close file:", e, t), delete this.filesWaitingForClose[e], t;
}
}
async deleteFile(e) {
await this.closeWritableFileHandle(e.id), await (await this.getFileHandle(e)).remove();
}
async write(e, t) {
await (await this.getWritableFileHandle(e)).write({
type: "write",
data: t.buffer,
position: t.position,
size: t.size
});
}
async getDirectoryHandle({ path: e }) {
try {
let t;
if (e !== void 0) {
const r = e.split("/");
this.packageName && r.unshift(this.packageName), t = await this.stepThrough(r, this.directoryHandle);
} else this.packageName ? t = await this.stepThrough([this.packageName], this.directoryHandle) : t = this.directoryHandle;
return t;
} catch (t) {
throw t;
}
}
async getParentDirectoryName(e) {
let r = e, s = 0;
for (; s <= 9; ) {
try {
await this.directoryHandle.getDirectoryHandle(r);
} catch (a) {
if (a.name === "NotFoundError") return r;
if (a.name !== "TypeMismatchError") throw a;
}
s++, r = \`\${this.packageName} (\${s})\`;
}
return "";
}
async stepThrough(e, t) {
const r = e.shift();
if (!r) throw new Error("No directory to step into");
const s = await t.getDirectoryHandle(
r,
{ create: !0 }
);
return e.length ? await this.stepThrough(e, s) : s;
}
}
var h = /* @__PURE__ */ ((i) => (i.Created = "worker:create", i.Cancelled = "worker:cancelled", i.Execute = "worker:execute", i.Finish = "worker:finish", i.FinishFile = "worker:finish-file", i.FinishChunk = "worker:finish-chunk", i.LinkExpired = "worker:link-expired", i.Error = "worker:error", i.Retry = "worker:retry", i.Progress = "worker:progress", i.Terminate = "worker:terminate", i.Abort = "worker:abort", i.ParentDirectoryCreated = "worker:parent-directory-created", i))(h || {});
const q = 6;
class P extends Error {
constructor(e) {
super(e), this.name = "ShortReadError";
}
}
const C = /* @__PURE__ */ new Set([
"QuotaExceededError",
// Disk is full
"NoModificationAllowedError",
// Read-only permissions error
"InvalidStateError"
// File(s) changed out of band
]);
async function L({
payload: i
}, e) {
try {
const { directoryHandle: t, packageName: r, packageId: s } = i;
e.fileHandleManager || (e.fileHandleManager = new T(t, r));
const a = await e.fileHandleManager.createParentDirectory(r, s);
e.postMessage({
event: h.ParentDirectoryCreated,
eventPayload: {
parentDirectoryName: a
}
});
} catch (t) {
console.error(
\`Error with download worker when creating parent directory: \${t}\`
), e.postMessage({
event: h.Error,
eventPayload: {
errorMsg: t.message
}
});
}
}
async function U({ payload: i }, e) {
const {
file: t,
directoryHandle: r,
packageName: s,
chunkSize: a,
retryAttempt: d,
finishedChunks: p,
chunkConcurrency: u
} = i;
e.fileHandleManager || (e.fileHandleManager = new T(r, s));
const o = crypto.randomUUID(), l = O(o, t.id, e), c = [];
try {
if (l.aborted) {
delete e.attempts[o];
return;
}
if (t.kind === "directory") {
await e.fileHandleManager.createEmptyDirectory(t), e.postMessage({
event: h.FinishFile,
eventPayload: {
fileId: t.id,
fileSize: 0
}
}), delete e.attempts[o];
return;
}
if (t.virus_detected) {
e.postMessage({
event: h.FinishFile,
eventPayload: {
fileId: t.id,
fileSize: t.size,
errorMsg: \`File with id \${t.id} contains a virus, download prevented.\`
}
}), delete e.attempts[o];
return;
}
const n = await e.fileHandleManager.getFileHandle(t);
if (l.aborted) {
delete e.attempts[o];
return;
}
if (!t.size) {
e.postMessage({
event: h.FinishFile,
eventPayload: {
fileId: t.id,
fileSize: 0
}
}), delete e.attempts[o];
return;
}
await e.fileHandleManager.getWritableFileHandle(t, n);
const y = Math.ceil(t.size / a), w = new Array(y).fill("").map((F, g) => async () => {
if (l.aborted)
return Promise.reject(new f("Download aborted"));
const M = g * a, v = Math.min(t.size - 1, M + a - 1);
return D(
{
file: t,
chunkIndex: g,
start: M,
end: v
},
void 0,
e,
o
).then(() => {
if (l.aborted)
return Promise.reject(new f("Download aborted"));
const H = w.shift();
return H ? H() : Promise.resolve();
});
}).filter((F, g) => !p.has(g) && !x(t.id, g, e));
if (!w.length) {
delete e.attempts[o];
return;
}
const R = u || q, A = Math.min(w.length, R);
for (let F = 0; F < A; F++) {
const g = w.shift();
g && c.push(g());
}
if (await Promise.all(c), l.aborted) {
delete e.attempts[o];
return;
}
await e.fileHandleManager.closeWritableFileHandle(t.id);
let b = "";
(await (await e.fileHandleManager.getFileHandle(t)).getFile()).size !== t.size ? b = "File size on disk does not match the file size on the package" : e.finishedChunks.delete(t.id), e.postMessage({
event: h.FinishFile,
eventPayload: {
fileId: t.id,
fileSize: t.size,
errorMsg: b
}
}), delete e.attempts[o];
} catch (n) {
if (c.length && await Promise.allSettled(c), n instanceof f) {
delete e.attempts[o];
return;
}
if (C.has(n.name)) {
e.postMessage({
event: h.Error,
eventPayload: {
errorMsg: n.message
}
}), delete e.attempts[o];
return;
}
if (n instanceof TypeError && n.message.includes("Failed to fetch")) {
e.postMessage({
event: h.LinkExpired,
eventPayload: {
fileId: t.id,
retryAttempt: d
}
}), delete e.attempts[o];
return;
}
try {
await e.fileHandleManager.deleteFile(t);
} catch (y) {
console.error("Error removing file handle after file error:", y);
}
e.postMessage({
event: h.FinishFile,
eventPayload: {
fileId: t.id,
fileSize: t.size,
errorMsg: n.message
}
}), e.finishedChunks.delete(t.id), delete e.attempts[o];
}
}
async function N({ payload: i }, e) {
const { requestType: t, deleteFiles: r } = i;
for (const s in e.attempts) {
const a = e.attempts[s];
a.aborted = !0, a.abortController.abort();
}
if (t === "Cancel") {
try {
if (e.finishedChunks.clear(), !e.fileHandleManager) return;
r && await e.fileHandleManager.deleteParentDirectory();
} catch {
return;
}
e.postMessage({
event: h.Cancelled,
eventPayload: {}
});
}
}
function O(i, e, t) {
const r = {
id: i,
fileId: e,
aborted: !1,
abortController: new AbortController()
};
for (const s in t.attempts) {
const a = t.attempts[s];
if (!a.aborted && a.fileId === e)
return r.aborted = !0, r.abortController.abort(), t.attempts[i] = r, r;
}
return t.attempts[i] = r, t.currentAttempt = i, r;
}
function $(i) {
return !(i >= 200 && i < 300);
}
function _(i) {
const e = i.message, t = i.status, r = i.name, s = [-1, 0];
return C.has(r) || i instanceof f ? !1 : i instanceof P ? !0 : i instanceof TypeError && e.includes("Failed to fetch") ? !navigator.onLine : i instanceof TypeError && e.includes("network error") ? !0 : t >= 500 || s.includes(t);
}
async function z(i, e, t = 0, r, s, a, d) {
if (!i.body)
throw new Error("Response body is missing");
const p = i.body.getReader({ mode: "byob" });
let u = new ArrayBuffer(r), o = 0;
const l = d.attempts[a];
if (!l || l.aborted)
throw new f("Download aborted");
if (d.fileHandleManager)
try {
for (; ; ) {
const { done: c, value: n } = await p.read(
new Uint8Array(u, o, u.byteLength - o)
);
if (l.aborted)
throw new f("Download aborted");
if (c)
throw new P("Short read");
if (d.postMessage({
event: h.Progress,
eventPayload: { fileId: e.id, chunkIndex: s, transferred: n.byteLength }
}), u = n.buffer, o += n.byteLength, o >= r) {
if (await d.fileHandleManager.write(e, {
buffer: u,
position: t,
size: r
}), l.aborted)
throw new f("Download aborted");
X(e.id, s, d), d.postMessage({
event: h.FinishChunk,
eventPayload: { fileId: e.id, chunkIndex: s, chunkSize: r }
});
break;
}
}
} catch (c) {
throw c.name !== "AbortError" && c.name !== "ShortReadError" && console.error("Error with read stream", c), d.postMessage({
event: h.Abort,
eventPayload: {
fileId: e.id,
chunkIndex: s,
lostProgress: o
}
}), c;
}
}
function X(i, e, t) {
let r = t.finishedChunks.get(i);
r || (r = /* @__PURE__ */ new Set()), r.add(e), t.finishedChunks.set(i, r);
}
function x(i, e, t) {
const r = t.finishedChunks.get(i);
return r ? r.has(e) : !1;
}
async function D({
file: i,
chunkIndex: e,
start: t,
end: r
}, s = 0, a, d) {
if (!a.fileHandleManager) return;
const l = a.attempts[d];
if (!l.aborted)
try {
const c = await fetch(i.url, {
keepalive: !0,
headers: {
range: \`bytes=\${t}-\${r}\`
},
signal: l.abortController.signal
});
if ($(c.status)) {
const n = new Error(
\`Request failed with status: \${c.status}\`
);
throw n.status = c.status, n;
} else if (c.status === 200 || c.status === 206) {
const n = r - t + 1;
await z(
c,
i,
t,
n,
e,
d,
a
);
return;
} else {
const n = new Error(
\`Unexpected response status code: \${c.status}\`
);
throw n.status = c.status, n;
}
} catch (c) {
if (l.aborted || c.name === "AbortError")
return;
if (!_(c))
throw console.error(
\`Error cannot be retried for chunk at range \${t}-\${r}: \${c.message}\`
), c;
let n = Math.pow(2, s) * 1e3;
return n > 3e4 && (n = 3e4), console.error(\`Error: \${c.message} for file \${i.id}. Retry Count: \${s}\`), a.postMessage({
event: h.Retry,
eventPayload: {
fileId: i.id,
chunkIndex: e,
errorMsg: c.message
}
}), new Promise((y) => {
setTimeout(() => {
y(
D(
{ file: i, chunkIndex: e, start: t, end: r },
s + 1,
a,
d
)
);
}, n);
});
}
}
const W = {
attempts: {},
currentAttempt: "",
fileHandleManager: void 0,
finishedChunks: /* @__PURE__ */ new Map(),
postMessage: self.postMessage.bind(self)
};
self.onmessage = async ({
data: i
}) => {
const { message: e, payload: t } = i, r = {
QueueFile: U,
Abort: N,
CreateParentDirectory: L
};
r[e] && await r[e]({ payload: t }, W);
};
`,q=typeof self<"u"&&self.Blob&&new Blob(["URL.revokeObjectURL(import.meta.url);",I],{type:"text/javascript;charset=utf-8"});function _(r){let e;try{if(e=q&&(self.URL||self.webkitURL).createObjectURL(q),!e)throw"";const t=new Worker(e,{type:"module",name:r?.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:r?.name})}}class z extends P{constructor(){super(),this.terminated=!1,this.onNativeWorkerMessage=({data:e})=>{const{event:t,eventPayload:n}=e;this.emit(t,{event:t,data:n})},this.emit(c.Created),this.startWorker()}startWorker(){this.nativeWorker=new _,this.nativeWorker.onmessage=this.onNativeWorkerMessage}abort(e){const t={message:b.Abort,payload:e};this.nativeWorker.postMessage(t)}createParentDirectory(e){const t={message:b.CreateParentDirectory,payload:e};this.nativeWorker.postMessage(t)}terminate(){this.terminated=!0,this.emit(c.Terminate),this.clearHandlers(),this.nativeWorker.terminate()}queueFile(e){if(this.terminated)throw new Error("Can't use a terminated worker");this.emit(c.Execute,{event:c.Execute,data:{requestPayload:e}});const t={message:b.QueueFile,payload:e};this.nativeWorker.postMessage(t)}}class U extends P{constructor(e,t,n=6,i=6){super(),this.packageId=e,this.chunkSize=t,this.poolSize=n,this.chunkConcurrency=i,this.workers=new Array,this.terminated=!1;let s;for(let a=0;a<this.poolSize;a++)s=new z,s.on("emit",this.bubbleEmit.bind(this)),this.workers.push(s)}prepareDownload(e,t,n){for(const s of this.workers)s.startWorker();if(n)return;const i={directoryHandle:e,packageName:t,packageId:this.packageId};this.workers[0].createParentDirectory(i)}queueFile(e,t,n,i,s=0){if(this.terminated)throw new S;const a=this.workers[e.index%this.workers.length],h={file:e.file,directoryHandle:n,packageName:i,chunkSize:this.chunkSize,retryAttempt:s,finishedChunks:t,chunkConcurrency:this.chunkConcurrency};a.queueFile(h)}abort(e,t,n,i){const s={requestType:e,directoryHandle:t,packageName:n,deleteFiles:i};this.workers.forEach(a=>{a.abort(s)})}terminate(){this.terminated=!0,this.workers.forEach(e=>e.terminate()),this.clearHandlers()}get workerPoolSize(){return this.poolSize}}class N{constructor(){this.queue={}}getFile(e){return this.queue[e]}addFile(e){const t=new O(e);return this.queue[e.fileId]=t,t}removeFile(e){delete this.queue[e]}setProgress(e,t,n){const i=this.getFile(e);i&&i.addChunkProgress(t,n)}get length(){return Object.keys(this.queue).length}}class O{constructor({fileId:e,index:t,file:n,chunkSize:i}){this.chunkProgress={},this.finishedChunks=new Set,this.id=e,this.file=n,this.index=t,n.size?this.chunkSize=i>n.size?n.size:i:this.chunkSize=i}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 x="masv-web",$="masv-web-downloader",W="dev",Q=100*1024*1024,D=6,j=D,X=1,B=100,A=300,G=200,J=100;var E=(r=>(r.Init="init",r.Idle="idle",r.Downloading="downloading",r.Ready="ready",r.Paused="paused",r.Finished="finished",r.PartialFinished="partial-finished",r.Errored="errored",r.Terminated="terminated",r.Cancelled="cancelled",r))(E||{});const k=class k extends P{constructor(e,t="",n="",i="https://api.massive.app",s=Q,a=`${x}/${W}`){super(),this.linkPassword=t,this.userToken=n,this.chunkSize=s,this.version="1.0.4",this.packageID="",this.packageToken="",this.packageName="",this.packageSize=0,this.directoryHandle=null,this.fileQueue=[],this.fileCount=0,this.isPartialDownload=!1,this.progressTracker=new H,this.filesFinished=0,this.currentFileIndex=0,this.currentFetchedFileIndex=0,this.fileMap={},this.pendingLinkQueue=[],this.requestingLinks=!1,this.erroredFiles=[],this.fileConcurrency=this.getFileConcurrency(),this.chunkConcurrency=this.getChunkConcurrency(),this.status="init",this.downloadState=new N,this.masvUserAgent=`${a}+${$}/${this.version}`;try{this.apiClient=new C(i,this.masvUserAgent);const h=new URL(e);if(this.linkID=h.pathname.slice(1),this.linkSecret=h.searchParams.get("secret")??"",!this.linkSecret||!this.linkID)throw new Error("Invalid download link")}catch(h){throw this.status="terminated",h}}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 U(this.packageID,this.chunkSize,this.fileConcurrency,this.chunkConcurrency),this.fdownloader.on(c.FinishFile,this.onFileFinish.bind(this)),this.fdownloader.on(c.FinishChunk,this.onChunkFinish.bind(this)),this.fdownloader.on(c.Progress,this.onFileProgress.bind(this)),this.fdownloader.on(c.Error,this.onDownloadError.bind(this)),this.fdownloader.on(c.Retry,this.onDownloadRetry.bind(this)),this.fdownloader.on(c.Abort,this.onDownloadAbort.bind(this)),this.fdownloader.on(c.ParentDirectoryCreated,this.onCreateParentDirectory.bind(this)),this.fdownloader.on(c.Cancelled,this.onDownloadCancelled.bind(this)),this.fdownloader.on(c.LinkExpired,this.onDownloadLinkExpired.bind(this)),this.fdownloader.on("emit",this.bubbleEmit.bind(this))}catch(e){this.handleError(e.message),this.cancel(),this.terminate()}}changeStatus(e){this.status=e,this.emit(this.status)}async onCreateParentDirectory(e){const{data:t}=e;this.parentDirectoryName=t.parentDirectoryName,this.emit(o.ParentDirectoryCreated,{event:o.ParentDirectoryCreated,data:{parentDirectoryName:this.parentDirectoryName}}),await this.startDownload()}onDownloadCancelled(e){this.changeStatus("cancelled")}async onFileFinish(e){try{const{data:t}=e,{fileId:n,fileSize:i,errorMsg:s}=t;this.filesFinished++;const a=this.downloadState.getFile(n);if(s?(console.error(`Error downloading file ${n}: ${s}`),a?.totalProgress&&this.progressTracker.subtractProgress(a?.totalProgress),this.erroredFiles.push(this.fileMap[n]),this.emit(o.FileErrored,{event:o.FileErrored,data:{fileId:n,errorMsg:s}})):(this.progressTracker.addFileProgress(i),this.emit(o.FileDownloaded,{event:o.FileDownloaded,data:{fileId:n}})),a&&this.downloadState.removeFile(a.id),this.filesFinished!=this.fileCount){await this.processQueue();return}if(this.progressTracker.mark(g.Finish),this.erroredFiles.length){this.emit(o.PartialFinished,{event:o.PartialFinished,data:{performanceStats:this.performanceStats,failedFiles:this.erroredFiles}}),this.changeStatus("partial-finished");const h={error_code:"partial_download",error_message:"Some files failed to be downloaded",event_time: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.isPartialDownload||await this.apiClient.notifyLinkDownloaded(this.linkID,this.linkSecret,this.linkPassword,this.userToken).catch(h=>console.error(h)),this.terminate(),this.changeStatus("finished")}catch(t){if(t instanceof u)return;this.handleError(t.message),this.cancel(),this.terminate()}}onChunkFinish(e){const{data:t}=e,{fileId:n,chunkIndex:i,chunkSize:s}=t,a=this.downloadState.getFile(n);a&&(this.downloadState.setProgress(n,i,s),a.writeChunk(i),this.progressTracker.addChunkProgress(s))}onFileProgress(e){const{data:t}=e,{transferred:n}=t;this.progressTracker.addProgress(n),this.emit(o.Progress,{event:o.Progress,data:{performanceStats:this.performanceStats}})}onDownloadError(e){const{data:t}=e,{errorMsg:n}=t;this.handleError(n),this.cancel(),this.terminate()}onDownloadRetry(e){const{data:t}=e,{errorMsg:n}=t;this.emit(o.Retry,{event:o.Retry,data:{performanceStats:this.performanceStats,errorMsg:n}})}onDownloadAbort(e){const{data:t}=e,{fileId:n,lostProgress:i}=t;i>0&&this.progressTracker.subtractProgress(i),this.emit(o.Abort,{event:o.Abort,data:{fileId:n}})}async onDownloadLinkExpired(e){try{const{data:t}=e,{fileId:n,retryAttempt:i}=t,s=await this.apiClient.fetchDownloadLink(this.packageID,this.packageToken,n),a=this.downloadState.getFile(n);if(!a)return;this.downloadState.removeFile(a.id),a.file.url=s.url,await this.processQueue(a,i+1)}catch(t){if(t instanceof u)return;this.handleError(t.message),this.cancel(),this.terminate()}}get canDownloadFile(){return this.status==="ready"||this.status==="downloading"}get allDownloadLinksFetched(){return this.currentFetchedFileIndex>=Object.values(this.fileMap).length}isLowFileDensity(e){return e<=.1*this.chunkSize}isHighFileDensity(e){return e>=10*this.chunkSize}getFileConcurrency(e){if(e){if(this.isLowFileDensity(e))return D;if(this.isHighFileDensity(e))return 1}return j}getChunkConcurrency(e){if(e){if(this.isLowFileDensity(e))return 1;if(this.isHighFileDensity(e))return D}return X}async fetchMetaDataLinks(){const t=Object.values(this.fileMap).filter(n=>n.kind===m.Metadata).map(n=>n.id);if(t.length)for(const n of t){const i=await this.apiClient.fetchDownloadLink(this.packageID,this.packageToken,n);this.pendingLinkQueue.push({fileId:n,url:i.url})}}chunkFile(e,t){const n=Math.min(t,e.size);return{fileId:e.id,index:this.currentFileIndex,file:e,chunkSize:n}}async processQueue(e,t=0){if(!this.canDownloadFile)return;if(!this.directoryHandle||!this.parentDirectoryName)throw new Error("No directory provided.");let n,i;if(e)i=e.file,n={fileId:i.id,index:e.index,file:i,chunkSize:this.chunkSize};else{const a=this.fileQueue.pop();if(!a)return;i=a,n=this.chunkFile(i,this.chunkSize)}this.emit(o.FileQueued,{event:o.FileQueued,data:{fileId:i.id}});const s=e?e.finishedChunks:new Set;this.fdownloader.queueFile(n,s,this.directoryHandle,this.parentDirectoryName,t),this.downloadState.addFile(n),e||this.currentFileIndex++,await this.fillFileQueue()}queueEmptyFilesAndDirectories(){const e=Object.values(this.fileMap).filter(t=>t.kind===m.Directory||t.kind===m.File&&!t.size);this.fileQueue=this.fileQueue.concat(e)}async fetchDownloadLinks(e){if(this.requestingLinks||(this.requestingLinks=!0,this.pendingLinkQueue.length>=A))return;const t=Math.min(Object.values(this.fileMap).length,e),n=Object.values(this.fileMap);let i=this.currentFetchedFileIndex+t;i>n.length&&(i=n.length);const s=n.filter(h=>h.kind===m.File&&h.size).slice(this.currentFetchedFileIndex,i).map(h=>h.id);if(this.currentFetchedFileIndex+=e,!s.length)return;const a=await this.apiClient.fetchDownloadLinks(this.packageID,this.packageToken,s);for(const h of a)this.pendingLinkQueue.push({fileId:h.id,url:h.url});this.requestingLinks=!1}async fillFileQueue(){for(;this.fileQueue.length<J;){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<G&&await this.fetchDownloadLinks(B)}async prepareDownload(e,t){this.filesFinished=0,this.currentFileIndex=0,this.currentFetchedFileIndex=0,this.progressTracker.reset(),this.progressTracker.setTotal(this.totalSize),this.progressTracker.setTotalFiles(this.fileCount),this.erroredFiles=[],this.queueEmptyFilesAndDirectories(),await this.fetchMetaDataLinks(),await this.fetchDownloadLinks(A-this.pendingLinkQueue.length),await this.fillFileQueue(),this.directoryHandle=e,this.progressTracker.mark(g.Start),this.fdownloader.prepareDownload(e,this.parentDirectoryName,t)}async getPackageInfo(){const e=await this.apiClient.fetchLinkDetails(this.linkID,this.linkSecret,this.linkPassword,this.userToken);return this.packageID=e.package_id,this.packageName=e.name,this.packageToken=e.package_token,this.packageSize=e.package_size,e}handleError(e){console.error(e),this.changeStatus("errored"),this.emit(o.Error,{event:o.Error,data:{performanceStats:this.performanceStats,errorMsg:e}});const t={error_code:"download_error",error_message:e,event_time:new Date().toISOString(),extras:this.progressSummary};this.apiClient.notifyError(this.packageID,this.packageToken,t)}pause(){try{if(this.status==="paused"||!this.directoryHandle||!this.parentDirectoryName)return;this.changeStatus("paused"),this.progressTracker.mark(g.Pause),this.fdownloader.abort(v.Pause,this.directoryHandle,this.parentDirectoryName,!1),this.apiClient.abortAll()}catch(e){if(e instanceof u)return;this.handleError(e.message),this.cancel(),this.terminate()}}cancel(e=!1){try{if(this.status==="cancelled"||!this.directoryHandle||!this.parentDirectoryName)return;this.changeStatus("cancelled"),this.progressTracker.mark(g.Pause),this.fdownloader.abort(v.Cancel,this.directoryHandle,this.parentDirectoryName,e),this.apiClient.abortAll();const t={error_code:"download_cancelled",error_message:"The download was manually cancelled by the user",event_time:new Date().toISOString(),extras:this.progressSummary};this.apiClient.notifyError(this.packageID,this.packageToken,t)}catch(t){if(t instanceof u)return;this.handleError(t.message),this.terminate()}}stop(){this.progressTracker.mark(g.Stop),this.changeStatus("idle")}terminate(){try{if(this.status==="terminated")return;this.changeStatus("terminated"),this.clearHandlers(),this.fdownloader.terminate()}catch(e){this.handleError(e.message)}}async loadFiles(){try{let e=await this.apiClient.fetchPackageFiles(this.packageID,this.packageToken);e||(e=[]);const t={};for(const n of e)t[n.id]=n;return this.fileMap=t,this.fileCount=Object.values(this.fileMap).filter(n=>!n.kind.includes("zip")).length,e}catch(e){throw this.handleError(e.message),this.cancel(),this.terminate(),e}}async retry(){try{if(this.status!="partial-finished")return;this.changeStatus("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){if(e instanceof u)return;this.handleError(e.message),this.cancel(),this.terminate()}}async start(e,t){const n=this.status==="init";try{if(this.status==="downloading")return;if(this.changeStatus("downloading"),this.parentDirectoryName||(this.parentDirectoryName=this.packageName),!n){this.progressTracker.mark(g.Continue);const i=Object.values(this.downloadState.queue);for(const a of i)await this.processQueue(a);const s=Math.min(this.fileConcurrency,Object.values(this.fileMap).length);for(let a=0;a<s-i.length;a++)await this.processQueue();return}if(t?.length){const i=this.fileCount,s={};for(const a of t)s[a.id]=a;this.fileMap=s,this.fileCount=Object.values(this.fileMap).filter(a=>!a.kind.includes("zip")).length,this.fileCount<i&&(this.isPartialDownload=!0)}await this.prepareDownload(e)}catch(i){if(i instanceof u)return;this.handleError(i.message),this.cancel(),this.terminate()}}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,n)=>t+(n.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)}}};k.DownloaderEvents=o,k.Emit="emit",k.States=E;let T=k;class S extends Error{constructor(){super("Terminated"),Object.setPrototypeOf(this,S.prototype)}}return w.Downloader=T,w.DownloaderStates=E,w.TerminatedError=S,Object.defineProperty(w,Symbol.toStringTag,{value:"Module"}),w}({});