@masvio/downloader
Version:
A simple, lightweight library to easily download files from MASV links
361 lines (360 loc) • 30.7 kB
JavaScript
var MASV=function(p){"use strict";class E{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}movingAverage(){return this.ma}}var f=(n=>(n.Start="download_start",n.Stop="download_stop",n.Pause="download_pause",n.Finish="download_finish",n.Continue="download_continue",n))(f||{});const m=500;class L{constructor(){this.total=0,this.marks={download_start:[],download_pause:[],download_continue:[],download_finish:[],download_stop:[]},this.moving=new E(m),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 E(m),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/(m/1e3),this.lastSampleAmount=this.progress,this.moving.push(t,this.instantSpeed),this.movingSpeed=this.moving.movingAverage(),setTimeout(()=>{this.debounceAverage=!1},m)}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}}get averageSpeed(){return 8*this.progress/(this.duration/1e3)}}var o=(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=(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 S{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=(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,k)=>{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){k(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(C=>h(C)).catch(C=>k(C))},d)}else try{let d;c.responseText&&(d=JSON.parse(c.responseText)),h(d)}catch(d){k(d)}},c.onerror=()=>{k(new Error(`Request failed with status ${c.status}`))},c.onabort=()=>{k(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 F=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);
}
`,D=typeof self<"u"&&self.Blob&&new Blob(["URL.revokeObjectURL(import.meta.url);",I],{type:"text/javascript;charset=utf-8"});function T(n){let e;try{if(e=D&&(self.URL||self.webkitURL).createObjectURL(D),!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 v=(n=>(n.Cancel="Cancel",n.Pause="Pause",n))(v||{});class A extends S{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 T,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 z extends S{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 A,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 y;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 R{constructor(){this.queue={}}getFile(e){return this.queue[e]}addFile(e){const t=new q(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 q{constructor({fileId:e,index:t,file:r,chunkSize:s}){this.chunkProgress={},this.finishedChunks=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 x=100*1024*1024,O=6,_=6,N=100,M=300,$=200,H=100;var b=(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))(b||{});const w=class w extends S{constructor(e,t="",r="https://api.massive.app",s=x,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 L,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 R,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 F(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"),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 z(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"),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")}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");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.changeStatus("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"),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"),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"),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<=.1*this.chunkSize}isHighFileDensity(e){return e>=10*this.chunkSize}getFileConcurrency(e){return e?this.isLowFileDensity(e)?12:this.isHighFileDensity(e)?1:6:O}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}}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: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>=M))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<H;){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<$&&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(M-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"),this.progressTracker.mark(f.Pause),this.fdownloader.abort(v.Pause,this.directoryHandle,this.parentDirectoryName,!1)}catch(e){console.error(e),this.cancel(),this.terminate(),this.changeStatus("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"),this.progressTracker.mark(f.Pause),this.fdownloader.abort(v.Cancel,this.directoryHandle,this.parentDirectoryName,e)}catch(t){console.error(t),this.terminate(),this.changeStatus("errored"),this.emit(o.Error,{event:o.Error,data:{performanceStats:this.performanceStats,error:t}})}}stop(){this.progressTracker.mark(f.Stop),this.changeStatus("idle")}terminate(){try{if(this.status==="terminated")return;this.changeStatus("terminated"),this.clearHandlers(),this.fdownloader.terminate()}catch(e){this.changeStatus("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"),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");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"),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"),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"),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=b;let P=w;class y extends Error{constructor(){super("Terminated"),Object.setPrototypeOf(this,y.prototype)}}return p.Downloader=P,p.DownloaderStates=b,p.TerminatedError=y,Object.defineProperty(p,Symbol.toStringTag,{value:"Module"}),p}({});