UNPKG

ipull

Version:

The only file downloader you'll ever need. For node.js and the browser, CLI and library for fast and reliable file downloads.

149 lines 5.1 kB
import UrlInputError from "./error/url-input-error.js"; import { EventEmitter } from "eventemitter3"; import ProgressStatisticsBuilder from "../../transfer-visualize/progress-statistics-builder.js"; import StatusCodeError from "../streams/download-engine-fetch-stream/errors/status-code-error.js"; import { InvalidOptionError } from "./error/InvalidOptionError.js"; import { promiseWithResolvers } from "../utils/promiseWithResolvers.js"; const IGNORE_HEAD_STATUS_CODES = [405, 501, 404]; export default class BaseDownloadEngine extends EventEmitter { options; _engine; _progressStatisticsBuilder = new ProgressStatisticsBuilder(); /** * @internal */ _downloadEndPromise = promiseWithResolvers(); /** * @internal */ _downloadStarted = false; _latestStatus; constructor(engine, options) { super(); this.options = options; this._engine = engine; this._progressStatisticsBuilder.add(engine); this._initEvents(); } get file() { return this._engine.file; } get downloadSize() { return this._engine.downloadSize; } get fileName() { return this.file.localFileName; } get status() { return this._latestStatus ?? ProgressStatisticsBuilder.oneStatistics(this._engine); } get downloadStatues() { return [this.status]; } /** * @internal */ get _fileEngineOptions() { return this._engine.options; } _initEvents() { this._engine.on("start", () => { return this.emit("start"); }); this._engine.on("save", (info) => { return this.emit("save", info); }); this._engine.on("finished", () => { return this.emit("finished"); }); this._engine.on("closed", () => { return this.emit("closed"); }); this._engine.on("paused", () => { return this.emit("paused"); }); this._engine.on("resumed", () => { return this.emit("resumed"); }); this._progressStatisticsBuilder.on("progress", (status) => { this._latestStatus = status; this.emit("progress", status); }); } async download() { if (this._downloadStarted) { return this._downloadEndPromise.promise; } try { this._downloadStarted = true; const promise = this._engine.download(); promise .then(this._downloadEndPromise.resolve) .catch(this._downloadEndPromise.reject); await promise; } finally { await this.close(); } } pause() { return this._engine.pause(); } resume() { return this._engine.resume(); } close() { return this._engine.close(); } static async _createDownloadFile(parts, fetchStream, { reuseRedirectURL } = {}) { const localFileName = decodeURIComponent(new URL(parts[0], "https://example").pathname.split("/") .pop() || ""); const downloadFile = { totalSize: 0, parts: [], localFileName }; downloadFile.parts = await Promise.all(parts.map(async (part, index) => { try { const { length, acceptRange, newURL, fileName } = await fetchStream.fetchDownloadInfo(part); const downloadURL = reuseRedirectURL ? (newURL ?? part) : part; const size = length || 0; downloadFile.totalSize += size; if (index === 0 && fileName) { downloadFile.localFileName = fileName; } return { downloadURL, size, acceptRange: size > 0 && acceptRange }; } catch (error) { if (error instanceof StatusCodeError && IGNORE_HEAD_STATUS_CODES.includes(error.statusCode)) { // if the server does not support HEAD request, we will skip that step return { downloadURL: part, size: 0, acceptRange: false }; } throw error; } })); return downloadFile; } static _validateURL(options) { if ("partURLs" in options && "url" in options) { throw new UrlInputError("Either `partURLs` or `url` should be provided, not both"); } if (!("partURLs" in options) && !("url" in options)) { throw new UrlInputError("Either `partURLs` or `url` should be provided"); } } static _validateOptions(options) { if ("tryHeaders" in options && options.tryHeaders?.length && "defaultFetchDownloadInfo" in options) { throw new InvalidOptionError("Cannot use `tryHeaders` with `defaultFetchDownloadInfo`"); } } } //# sourceMappingURL=base-download-engine.js.map