libmodpm
Version:
Modrinth package manager library
226 lines • 15.3 kB
JavaScript
// SPDX-License-Identifier: GPL-3.0-or-later
import { TypedEventTarget } from "./events/TypedEventTarget.js";
import { HTTPClient } from "./HTTPClient.js";
/**
* Represents a file to be downloaded.
*
* @final
*/
class Entry {
/**
* Direct download URL of the file.
*/
url;
/**
* SHA-512 hash of the file, encoded as a hex string.
*/
hash;
/**
* File name of the file.
*/
name;
/**
* Size of the file, in bytes.
*/
size;
/**
* Creates a new file entry.
*
* @param url Direct download URL of the file.
* @param hash SHA-512 hash of the file, encoded as a hex string.
* @param name File name of the file.
* @param [size] Size of the file, in bytes.
*/
constructor(url, hash, name, size) {
this.url = url;
this.hash = hash;
this.name = name;
this.size = size;
}
}
/**
* Represents a download error.
*
* @final
*/
class DownloadError extends Error {
/**
* Creates a new download error.
*
* @param message Error message.
*/
constructor(message) {
super(message);
this.name = new.target.name;
}
}
/**
* Downloads files from a list of URLs into a specified directory.
*
* @template T Error type.
* @final
*/
export class Downloader extends HTTPClient {
static Entry = Entry;
static DownloadError = DownloadError;
/**
* Downloader events.
*/
events = new TypedEventTarget();
/**
* Internal events.
*/
internalEvents = new TypedEventTarget();
/**
* Controller for aborting downloader workers.
*/
abortController = new AbortController();
/**
* Files to download.
*/
entries;
/**
* Number of files to download in parallel.
*/
concurrency;
/**
* Directory to download files into.
*/
directory;
/**
* Download progress.
*/
progress = new Map();
/**
* Creates a new downloader.
*
* @param userAgent User agent string used when making requests.
* @param entries Files to download.
* @param concurrency Number of files to download in parallel.
* @param directory Directory to download files into.
*/
constructor(userAgent, entries, concurrency, directory) {
super(userAgent);
this.entries = entries;
this.concurrency = concurrency;
this.directory = directory;
}
/**
* Aborts downloading.
*/
abort() {
this.abortController.abort();
}
/**
* Begins downloading the files.
*
* @returns `false` if downloading was interrupted and did not complete, `true` otherwise.
*/
async download() {
this.internalEvents.on("workerFailed", () => this.abort(), { once: true });
const workers = Array.from({ length: this.concurrency }, () => this.worker(this.abortController.signal));
return (await Promise.all(workers)).every(Boolean);
}
/**
* Retrieves file size and name by sending an HTTP HEAD request to the specified URL.
*
* @param url URL of the file.
* @returns File size from the `Content-Length` header (or `null` if not present) and file name from the
* `Content-Disposition` header or the last URI path component.
* @throws {@link DownloadError} If the request fails.
* @throws {@link !TypeError} If fetching fails.
*/
async getSizeAndName(url) {
const res = await this.fetch(url, { method: "HEAD" });
const size = res.headers.has("Content-Length")
? Number.parseInt(res.headers.get("Content-Length"))
: NaN;
const name = res.headers.get("Content-Disposition")
?.match(/filename\*?=(?:UTF-8''|")?([^;"\r\n]*)/i)?.[1]
?? globalThis.encodeURIComponent(url.pathname.split("/").pop());
return {
size: Number.isInteger(size) ? size : null,
name,
};
}
createError(res) {
return Promise.resolve(new DownloadError(`Status code: ${res.status} for ${res.url}`));
}
/**
* Creates a new download worker.
*
* @param signal Abort signal for the worker.
* @returns `false` if the worker terminated early due to error or abort, `true` otherwise.
*/
async worker(signal) {
while (true) {
if (signal.aborted)
break;
const file = this.entries.shift();
if (file === undefined)
return true;
this.events.dispatchEvent(new CustomEvent("start", { detail: file }));
const res = await this.fetch(file.url, { signal }).catch((e) => {
if (e.name === "AbortError") {
this.entries.unshift(file);
return null;
}
else if (e instanceof DownloadError)
this.events.dispatchEvent(new CustomEvent("downloadError", { detail: { entry: file, error: e } }));
else if (e.cause instanceof Error)
this.events.dispatchEvent(new CustomEvent("downloadError", {
detail: { entry: file, error: new DownloadError(e.cause.message) },
}));
else
this.events.dispatchEvent(new CustomEvent("downloadError", {
detail: { entry: file, error: new DownloadError(e.message) },
}));
return null;
});
if (res === null)
break;
this.progress.set(file.hash, 0);
// likely empty
if (res.body === null) {
await file.write(this.directory, await res.arrayBuffer());
this.events.dispatchEvent(new CustomEvent("progress", { detail: { entry: file, progress: 100 } }));
}
else {
const writeStream = await file.getStream(this.directory);
// stream without progress
if (file.size === undefined || file.size === 0) {
try {
await res.body.pipeTo(writeStream);
}
catch (e) {
break;
}
this.events.dispatchEvent(new CustomEvent("progress", { detail: { entry: file, progress: 100 } }));
}
// stream with progress
else
try {
await res.body.pipeThrough(new TransformStream({
transform: (chunk, controller) => {
const downloaded = this.progress.get(file.hash) + chunk.byteLength;
this.progress.set(file.hash, downloaded);
this.events.dispatchEvent(new CustomEvent("progress", {
detail: { entry: file, progress: downloaded / file.size * 100 },
}));
controller.enqueue(chunk);
},
})).pipeTo(writeStream);
}
catch (e) {
break;
}
}
this.events.dispatchEvent(new CustomEvent("end", { detail: file }));
if (!await file.exists(this.directory))
this.events.dispatchEvent(new CustomEvent("hashError", { detail: { entry: file } }));
}
this.internalEvents.dispatchEvent(new CustomEvent("workerFailed"));
return false;
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiRG93bmxvYWRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9Eb3dubG9hZGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLDRDQUE0QztBQUM1QyxPQUFPLEVBQUMsZ0JBQWdCLEVBQUMsTUFBTSw4QkFBOEIsQ0FBQztBQUM5RCxPQUFPLEVBQUMsVUFBVSxFQUFDLE1BQU0saUJBQWlCLENBQUM7QUFFM0M7Ozs7R0FJRztBQUNILE1BQWUsS0FBSztJQUNoQjs7T0FFRztJQUNhLEdBQUcsQ0FBTTtJQUV6Qjs7T0FFRztJQUNhLElBQUksQ0FBUztJQUU3Qjs7T0FFRztJQUNhLElBQUksQ0FBUztJQUU3Qjs7T0FFRztJQUNhLElBQUksQ0FBVTtJQUU5Qjs7Ozs7OztPQU9HO0lBQ0gsWUFBc0IsR0FBUSxFQUFFLElBQVksRUFBRSxJQUFZLEVBQUUsSUFBYTtRQUNyRSxJQUFJLENBQUMsR0FBRyxHQUFHLEdBQUcsQ0FBQztRQUNmLElBQUksQ0FBQyxJQUFJLEdBQUcsSUFBSSxDQUFDO1FBQ2pCLElBQUksQ0FBQyxJQUFJLEdBQUcsSUFBSSxDQUFDO1FBQ2pCLElBQUksQ0FBQyxJQUFJLEdBQUcsSUFBSSxDQUFDO0lBQ3JCLENBQUM7Q0F1Qko7QUFFRDs7OztHQUlHO0FBQ0gsTUFBTSxhQUFjLFNBQVEsS0FBSztJQUM3Qjs7OztPQUlHO0lBQ0gsWUFBbUIsT0FBZTtRQUM5QixLQUFLLENBQUMsT0FBTyxDQUFDLENBQUM7UUFDZixJQUFJLENBQUMsSUFBSSxHQUFHLEdBQUcsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDO0lBQ2hDLENBQUM7Q0FDSjtBQUVEOzs7OztHQUtHO0FBQ0gsTUFBTSxPQUFPLFVBQVcsU0FBUSxVQUF5QjtJQUM5QyxNQUFNLENBQVUsS0FBSyxHQUFHLEtBQUssQ0FBQztJQUM5QixNQUFNLENBQVUsYUFBYSxHQUFHLGFBQWEsQ0FBQztJQUVyRDs7T0FFRztJQUNhLE1BQU0sR0FBRyxJQUFJLGdCQUFnQixFQU16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