UNPKG

libmodpm

Version:

Modrinth package manager library

226 lines 15.3 kB
// 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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