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.

162 lines 7.22 kB
import BaseDownloadEngineFetchStream, { MIN_LENGTH_FOR_MORE_INFO_REQUEST, STREAM_NOT_RESPONDING_TIMEOUT } from "./base-download-engine-fetch-stream.js"; import InvalidContentLengthError from "./errors/invalid-content-length-error.js"; import SmartChunkSplit from "./utils/smart-chunk-split.js"; import { parseContentDisposition } from "./utils/content-disposition.js"; import StatusCodeError from "./errors/status-code-error.js"; import { parseHttpContentRange } from "./utils/httpRange.js"; import { browserCheck } from "./utils/browserCheck.js"; import { EmptyStreamTimeoutError } from "./errors/EmptyStreamTimeoutError.js"; import prettyMilliseconds from "pretty-ms"; export default class DownloadEngineFetchStreamFetch extends BaseDownloadEngineFetchStream { _fetchDownloadInfoWithHEAD = false; _activeController; transferAction = "Downloading"; supportDynamicStreamLength = true; withSubState(state) { const fetchStream = new DownloadEngineFetchStreamFetch(this.options); return this.cloneState(state, fetchStream); } async fetchWithoutRetryChunks(callback) { const headers = { accept: "*/*", ...this.options.headers }; if (this.state.rangeSupport) { headers.range = `bytes=${this._startSize}-${this._endSize - 1}`; } if (!this._activeController?.signal.aborted) { this._activeController?.abort(); } let response = null; this._activeController = new AbortController(); this.on("aborted", () => { if (!response) { this._activeController?.abort(); } }); response = await fetch(this.appendToURL(this.state.url), { headers, signal: this._activeController.signal }); if (response.status < 200 || response.status >= 300) { throw new StatusCodeError(this.state.url, response.status, response.statusText, headers); } const contentLength = parseHttpContentRange(response.headers.get("content-range"))?.length ?? parseInt(response.headers.get("content-length")); const expectedContentLength = this._endSize - this._startSize; if (this.state.rangeSupport && contentLength !== expectedContentLength) { throw new InvalidContentLengthError(expectedContentLength, contentLength); } const reader = response.body.getReader(); return await this.chunkGenerator(callback, () => reader.read()); } async fetchDownloadInfoWithoutRetry(url) { if (this._fetchDownloadInfoWithHEAD) { try { return this.fetchDownloadInfoWithoutRetryByMethod(url, "HEAD"); } catch (error) { if (!(error instanceof StatusCodeError)) { throw error; } this._fetchDownloadInfoWithHEAD = false; } } return this.fetchDownloadInfoWithoutRetryByMethod(url, "GET"); } async fetchDownloadInfoWithoutRetryByMethod(url, method = "HEAD") { const response = await fetch(url, { method: method, headers: { "Accept-Encoding": "identity", ...this.options.headers } }); if (response.status < 200 || response.status >= 300) { throw new StatusCodeError(url, response.status, response.statusText, this.options.headers, DownloadEngineFetchStreamFetch.convertHeadersToRecord(response.headers)); } const acceptRange = this.options.acceptRangeIsKnown ?? response.headers.get("accept-ranges") === "bytes"; const fileName = parseContentDisposition(response.headers.get("content-disposition")); let length = parseInt(response.headers.get("content-length")) || 0; const contentEncoding = response.headers.get("content-encoding"); if (contentEncoding && contentEncoding !== "identity") { length = 0; // If content is encoded, we cannot determine the length reliably } if (acceptRange && length === 0 && browserCheck() && MIN_LENGTH_FOR_MORE_INFO_REQUEST < length) { length = await this.fetchDownloadInfoWithoutRetryContentRange(url, method === "GET" ? response : undefined); } return { length, acceptRange, newURL: response.url, fileName }; } async fetchDownloadInfoWithoutRetryContentRange(url, response) { const responseGet = response ?? await fetch(url, { method: "GET", headers: { accept: "*/*", ...this.options.headers, range: "bytes=0-0" } }); const contentRange = responseGet.headers.get("content-range"); return parseHttpContentRange(contentRange)?.size || 0; } async chunkGenerator(callback, getNextChunk) { const smartSplit = new SmartChunkSplit(callback, this.state); // eslint-disable-next-line no-constant-condition while (true) { const chunkInfo = await this._wrapperStreamNotResponding(getNextChunk()); await this.paused; if (!chunkInfo || this.aborted || chunkInfo.done) break; smartSplit.addChunk(chunkInfo.value); this.state.onProgress?.(smartSplit.savedLength); } smartSplit.closeAndSendLeftoversIfLengthIsUnknown(); } _wrapperStreamNotResponding(promise) { if (!(promise instanceof Promise)) { return promise; } return new Promise((resolve, reject) => { let streamNotRespondedInTime = false; let timeoutMaxStreamWaitThrows = false; const timeoutNotResponding = setTimeout(() => { streamNotRespondedInTime = true; this.emit("streamNotRespondingOn"); }, STREAM_NOT_RESPONDING_TIMEOUT); const timeoutMaxStreamWait = setTimeout(() => { timeoutMaxStreamWaitThrows = true; reject(new EmptyStreamTimeoutError(`Stream timeout after ${prettyMilliseconds(this.options.maxStreamWait)}`)); this._activeController?.abort(); }, this.options.maxStreamWait); this.addListener("aborted", resolve); promise .then(resolve) .catch(error => { if (timeoutMaxStreamWaitThrows || this.aborted) { return; } reject(error); }) .finally(() => { clearTimeout(timeoutNotResponding); clearTimeout(timeoutMaxStreamWait); if (streamNotRespondedInTime) { this.emit("streamNotRespondingOff"); } this.removeListener("aborted", resolve); }); }); } static convertHeadersToRecord(headers) { const headerObj = {}; headers.forEach((value, key) => { headerObj[key] = value; }); return headerObj; } } //# sourceMappingURL=download-engine-fetch-stream-fetch.js.map