From 9326aaa4dea3c775361dd76f83c4bff3cf7577ee Mon Sep 17 00:00:00 2001 From: ido Date: Tue, 27 Feb 2024 18:08:40 +0200 Subject: [PATCH] fix: none async events --- package-lock.json | 18 ++-- package.json | 2 +- .../download-engine/download-engine-file.ts | 84 +++++++++++-------- .../engine/base-download-engine.ts | 56 ++++++++----- .../engine/download-engine-browser.ts | 8 +- .../engine/download-engine-multi-download.ts | 22 ++--- .../engine/download-engine-nodejs.ts | 21 ++--- .../base-download-engine-fetch-stream.ts | 8 +- .../download-engine-fetch-stream-fetch.ts | 4 +- ...download-engine-fetch-stream-local-file.ts | 4 +- .../download-engine-fetch-stream-xhr.ts | 4 +- src/download/download-engine/types.ts | 13 --- src/download/node-download.ts | 49 ++++++----- .../progress-statistics-builder.ts | 17 ++-- .../transfer-cli/transfer-cli-switch.ts | 12 +++ .../{ => transfer-cli}/transfer-cli.ts | 55 +++++++++--- src/index.ts | 2 +- 17 files changed, 223 insertions(+), 156 deletions(-) create mode 100644 src/download/transfer-visualize/transfer-cli/transfer-cli-switch.ts rename src/download/transfer-visualize/{ => transfer-cli}/transfer-cli.ts (73%) diff --git a/package-lock.json b/package-lock.json index 65e528d..09a508c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "async-retry": "^1.3.3", "chalk": "^5.3.0", "commander": "^10.0.0", - "emittery": "^1.0.3", + "eventemitter3": "^5.0.1", "fs-extra": "^11.1.1", "level": "^8.0.0", "lifecycle-utils": "^1.3.1", @@ -3577,17 +3577,6 @@ "readable-stream": "^2.0.2" } }, - "node_modules/emittery": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-1.0.3.tgz", - "integrity": "sha512-tJdCJitoy2lrC2ldJcqN4vkqJ00lT+tOWNT1hBJjO/3FDMJa5TTIiYGCKGkn/WfCyOzUMObeohbVTj00fhiLiA==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -4246,6 +4235,11 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + }, "node_modules/execa": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", diff --git a/package.json b/package.json index 7d96abb..3ebf6ea 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "async-retry": "^1.3.3", "chalk": "^5.3.0", "commander": "^10.0.0", - "emittery": "^1.0.3", + "eventemitter3": "^5.0.1", "fs-extra": "^11.1.1", "level": "^8.0.0", "lifecycle-utils": "^1.3.1", diff --git a/src/download/download-engine/download-engine-file.ts b/src/download/download-engine/download-engine-file.ts index 2f9cbc6..e8780b1 100644 --- a/src/download/download-engine/download-engine-file.ts +++ b/src/download/download-engine/download-engine-file.ts @@ -1,33 +1,46 @@ import {PromisePool, Stoppable} from "@supercharge/promise-pool"; import ProgressStatusFile from "./progress-status-file.js"; -import {ChunkStatus, DownloadEngineFileOptions, DownloadFile, DownloadProgressInfo} from "./types.js"; -import Emittery from "emittery"; - -export type DownloadEngineFileOptionsWithDefaults = DownloadEngineFileOptions & - { - chunkSize: number; - parallelStreams: number; - }; +import {ChunkStatus, DownloadFile, DownloadProgressInfo} from "./types.js"; +import BaseDownloadEngineFetchStream from "./streams/download-engine-fetch-stream/base-download-engine-fetch-stream.js"; +import BaseDownloadEngineWriteStream from "./streams/download-engine-write-stream/base-download-engine-write-stream.js"; +import retry from "async-retry"; +import {EventEmitter} from "eventemitter3"; + +export type DownloadEngineFileOptions = { + chunkSize?: number; + parallelStreams?: number; + retry?: retry.Options + comment?: string; + fetchStream: BaseDownloadEngineFetchStream, + writeStream: BaseDownloadEngineWriteStream, + onFinishAsync?: () => Promise + onCloseAsync?: () => Promise +}; -export interface DownloadEngineFileEvents { - start: undefined; - paused: undefined; - resumed: undefined; - progress: ProgressStatusFile; - save: DownloadProgressInfo; - finished: undefined; - closed: undefined; +export type DownloadEngineFileOptionsWithDefaults = DownloadEngineFileOptions & { + chunkSize: number; + parallelStreams: number; +}; - [key: string]: any; -} +export type DownloadEngineFileEvents = { + start: () => void + paused: () => void + resumed: () => void + progress: (progress: ProgressStatusFile) => void + save: (progress: DownloadProgressInfo) => void + finished: () => void + closed: () => void + [key: string]: any +}; const DEFAULT_OPTIONS: Omit = { chunkSize: 1024 * 1024 * 5, parallelStreams: 4 }; -export default class DownloadEngineFile extends Emittery { +export default class DownloadEngineFile extends EventEmitter { public readonly file: DownloadFile; + public options: DownloadEngineFileOptionsWithDefaults; protected _progress: DownloadProgressInfo = { part: 0, @@ -37,7 +50,6 @@ export default class DownloadEngineFile extends Emittery this._progress.part) { this._progress.part = i; - this._progress.chunkSize = this._options.chunkSize; + this._progress.chunkSize = this.options.chunkSize; this._progress.chunks = this._emptyChunksForPart(i); } @@ -109,12 +121,13 @@ export default class DownloadEngineFile extends Emittery { await this._pausedPromise; @@ -127,24 +140,24 @@ export default class DownloadEngineFile extends Emittery { + const buffer = await this.options.fetchStream.fetchBytes(this._activePart.downloadURL!, start, end, (length: number) => { this._activeStreamBytes[index] = length; this._sendProgressDownloadPart(); }); - await this._options.writeStream.write(start, buffer); + await this.options.writeStream.write(start, buffer); this._progress.chunks[index] = ChunkStatus.COMPLETE; delete this._activeStreamBytes[index]; - await this._saveProgress(); + this._saveProgress(); }); } finally { this._activePool = undefined; } } - protected async _saveProgress() { - await this.emit("save", this._progress); + protected _saveProgress() { + this.emit("save", this._progress); this._sendProgressDownloadPart(); } @@ -183,9 +196,10 @@ export default class DownloadEngineFile extends Emittery = - DownloadEngineFileOptions - & BaseDownloadEngineFetchStreamOptions - & - { - comment?: string; - fetchStrategy?: FetchStrategy; - }; - -export interface BaseDownloadEngineEvents extends Omit { - progress: TransferProgressWithStatus; -} -export default class BaseDownloadEngine extends Emittery { - public readonly options: BaseDownloadEngineOptions; +export type BaseDownloadEngineOptions = InputURLOptions & BaseDownloadEngineFetchStreamOptions & { + chunkSize?: number; + parallelStreams?: number; + retry?: retry.Options + comment?: string; +}; + +export type BaseDownloadEngineEvents = { + start: () => void + paused: () => void + resumed: () => void + progress: (progress: TransferProgressWithStatus) => void + save: (progress: DownloadProgressInfo) => void + finished: () => void + closed: () => void + [key: string]: any +}; + +export default class BaseDownloadEngine extends EventEmitter { + public readonly options: DownloadEngineFileOptions; protected readonly _engine: DownloadEngineFile; protected _progressStatisticsBuilder = new ProgressStatisticsBuilder(); protected _downloadStarted = false; @@ -36,7 +43,7 @@ export default class BaseDownloadEngine extends Emittery { return this.emit("start"); }); + this._engine.on("save", (info) => { + return this.emit("save", info); + }); this._engine.on("finished", () => { return this.emit("finished"); }); @@ -69,9 +79,13 @@ export default class BaseDownloadEngine extends Emittery, "fetchStream" | "writeStream"> & - InputURLOptions & { +export type DownloadEngineOptionsBrowser = BaseDownloadEngineOptions & { onWrite?: DownloadEngineWriteStreamBrowserWriter, - progress?: DownloadProgressInfo + progress?: DownloadProgressInfo, + fetchStrategy?: "xhr" | "fetch", }; export type DownloadEngineOptionsCustomFetchBrowser = DownloadEngineOptionsBrowser & { diff --git a/src/download/download-engine/engine/download-engine-multi-download.ts b/src/download/download-engine/engine/download-engine-multi-download.ts index bddc871..f12b130 100644 --- a/src/download/download-engine/engine/download-engine-multi-download.ts +++ b/src/download/download-engine/engine/download-engine-multi-download.ts @@ -1,14 +1,14 @@ import {BaseDownloadEngineEvents} from "./base-download-engine.js"; -import Emittery from "emittery"; +import {EventEmitter} from "eventemitter3"; import ProgressStatisticsBuilder, {AnyEngine} from "../../transfer-visualize/progress-statistics-builder.js"; import DownloadAlreadyStartedError from "./error/download-already-started-error.js"; -interface DownloadEngineMultiDownloadEvents extends BaseDownloadEngineEvents { - childDownloadStarted: Engine; - childDownloadClosed: Engine; -} +type DownloadEngineMultiDownloadEvents = BaseDownloadEngineEvents & { + childDownloadStarted: (engine: Engine) => void + childDownloadClosed: (engine: Engine) => void +}; -export default class DownloadEngineMultiDownload extends Emittery { +export default class DownloadEngineMultiDownload extends EventEmitter { protected _aborted = false; protected _activeEngine?: Engine; protected _progressStatisticsBuilder = new ProgressStatisticsBuilder(); @@ -34,16 +34,16 @@ export default class DownloadEngineMultiDownload, "writeStream" | "fetchStream"> - & PathOptions & InputURLOptions & { +export type DownloadEngineOptionsNodejs = PathOptions & BaseDownloadEngineOptions & { fileName?: string; + fetchStrategy?: "localFile" | "fetch"; }; export type DownloadEngineOptionsNodejsCustomFetch = DownloadEngineOptionsNodejs & { @@ -42,17 +42,18 @@ export default class DownloadEngineNodejs { - await this.options.writeStream.saveMedataAfterFile(data); + this._engine.on("save", (info) => { + this.options.writeStream.saveMedataAfterFile(info); }); - this._engine.on("finished", async () => { + this._engine.options.onFinishAsync = async () => { await this.options.writeStream.ftruncate(); - }); + }; - this._engine.on("closed", async () => { - await fs.rename(this.options.writeStream.path, this.options.writeStream.path.slice(0, -PROGRESS_FILE_EXTENSION.length)); - }); + this._engine.options.onCloseAsync = async () => { + const closedFileName = this.options.writeStream.path.slice(0, -PROGRESS_FILE_EXTENSION.length); + await fs.rename(this.options.writeStream.path, closedFileName); + }; } /** diff --git a/src/download/download-engine/streams/download-engine-fetch-stream/base-download-engine-fetch-stream.ts b/src/download/download-engine/streams/download-engine-fetch-stream/base-download-engine-fetch-stream.ts index 1db31b5..cb75bbc 100644 --- a/src/download/download-engine/streams/download-engine-fetch-stream/base-download-engine-fetch-stream.ts +++ b/src/download/download-engine/streams/download-engine-fetch-stream/base-download-engine-fetch-stream.ts @@ -17,19 +17,19 @@ export default abstract class BaseDownloadEngineFetchStream { public async fetchBytes(url: string, start: number, end: number, onProgress?: (length: number) => void) { return await retry(async () => { - return await this._fetchBytesWithoutRetry(url, start, end, onProgress); + return await this.fetchBytesWithoutRetry(url, start, end, onProgress); }, this.options.retry); } - protected abstract _fetchBytesWithoutRetry(url: string, start: number, end: number, onProgress?: (length: number) => void): Promise; + protected abstract fetchBytesWithoutRetry(url: string, start: number, end: number, onProgress?: (length: number) => void): Promise; public async fetchDownloadInfo(url: string): Promise<{ length: number, acceptRange: boolean }> { return this.options.defaultFetchDownloadInfo ?? await retry(async () => { - return await this._fetchDownloadInfoWithoutRetry(url); + return await this.fetchDownloadInfoWithoutRetry(url); }, this.options.retry); } - protected abstract _fetchDownloadInfoWithoutRetry(url: string): Promise<{ length: number, acceptRange: boolean }>; + protected abstract fetchDownloadInfoWithoutRetry(url: string): Promise<{ length: number, acceptRange: boolean }>; public close(): void | Promise { } diff --git a/src/download/download-engine/streams/download-engine-fetch-stream/download-engine-fetch-stream-fetch.ts b/src/download/download-engine/streams/download-engine-fetch-stream/download-engine-fetch-stream-fetch.ts index b3cb134..e25438e 100644 --- a/src/download/download-engine/streams/download-engine-fetch-stream/download-engine-fetch-stream-fetch.ts +++ b/src/download/download-engine/streams/download-engine-fetch-stream/download-engine-fetch-stream-fetch.ts @@ -2,7 +2,7 @@ import BaseDownloadEngineFetchStream from "./base-download-engine-fetch-stream.j import InvalidContentLengthError from "./errors/invalid-content-length-error.js"; export default class DownloadEngineFetchStreamFetch extends BaseDownloadEngineFetchStream { - protected async _fetchBytesWithoutRetry(url: string, start: number, end: number, onProgress?: (length: number) => void) { + protected async fetchBytesWithoutRetry(url: string, start: number, end: number, onProgress?: (length: number) => void) { const response = await fetch(this._appendToURL(url), { headers: { accept: "*/*", @@ -40,7 +40,7 @@ export default class DownloadEngineFetchStreamFetch extends BaseDownloadEngineFe return arrayBuffer; } - protected async _fetchDownloadInfoWithoutRetry(url: string): Promise<{ length: number; acceptRange: boolean }> { + protected async fetchDownloadInfoWithoutRetry(url: string): Promise<{ length: number; acceptRange: boolean }> { const response = await fetch(url, { method: "HEAD", ...this.options.headers diff --git a/src/download/download-engine/streams/download-engine-fetch-stream/download-engine-fetch-stream-local-file.ts b/src/download/download-engine/streams/download-engine-fetch-stream/download-engine-fetch-stream-local-file.ts index b975c6a..d9b4477 100644 --- a/src/download/download-engine/streams/download-engine-fetch-stream/download-engine-fetch-stream-local-file.ts +++ b/src/download/download-engine/streams/download-engine-fetch-stream/download-engine-fetch-stream-local-file.ts @@ -24,14 +24,14 @@ export default class DownloadEngineFetchStreamLocalFile extends BaseDownloadEngi }); } - protected async _fetchBytesWithoutRetry(path: string, start: number, end: number) { + protected async fetchBytesWithoutRetry(path: string, start: number, end: number) { const file = await this._ensureFileOpen(path); const buffer = Buffer.alloc(end - start); await file.read(buffer, 0, buffer.byteLength, start); return buffer; } - protected async _fetchDownloadInfoWithoutRetry(path: string): Promise<{ length: number; acceptRange: boolean }> { + protected async fetchDownloadInfoWithoutRetry(path: string): Promise<{ length: number; acceptRange: boolean }> { const stat = await fs.stat(path); if (!stat.isFile()) { throw new Error("Path is a directory"); diff --git a/src/download/download-engine/streams/download-engine-fetch-stream/download-engine-fetch-stream-xhr.ts b/src/download/download-engine/streams/download-engine-fetch-stream/download-engine-fetch-stream-xhr.ts index 097e786..d63eade 100644 --- a/src/download/download-engine/streams/download-engine-fetch-stream/download-engine-fetch-stream-xhr.ts +++ b/src/download/download-engine/streams/download-engine-fetch-stream/download-engine-fetch-stream-xhr.ts @@ -6,7 +6,7 @@ import InvalidContentLengthError from "./errors/invalid-content-length-error.js" export default class DownloadEngineFetchStreamXhr extends BaseDownloadEngineFetchStream { - protected _fetchBytesWithoutRetry(url: string, start: number, end: number, onProgress?: (length: number) => void): Promise { + protected fetchBytesWithoutRetry(url: string, start: number, end: number, onProgress?: (length: number) => void): Promise { return new Promise((resolve, reject) => { const headers = { accept: "*/*", @@ -54,7 +54,7 @@ export default class DownloadEngineFetchStreamXhr extends BaseDownloadEngineFetc }); } - protected _fetchDownloadInfoWithoutRetry(url: string): Promise<{ length: number; acceptRange: boolean; }> { + protected fetchDownloadInfoWithoutRetry(url: string): Promise<{ length: number; acceptRange: boolean; }> { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("HEAD", url, true); diff --git a/src/download/download-engine/types.ts b/src/download/download-engine/types.ts index 50af3e7..581ca28 100644 --- a/src/download/download-engine/types.ts +++ b/src/download/download-engine/types.ts @@ -1,7 +1,3 @@ -import retry from "async-retry"; -import BaseDownloadEngineFetchStream from "./streams/download-engine-fetch-stream/base-download-engine-fetch-stream.js"; -import BaseDownloadEngineWriteStream from "./streams/download-engine-write-stream/base-download-engine-write-stream.js"; - export type DownloadFilePart = { downloadURL?: string acceptRange?: boolean @@ -26,12 +22,3 @@ export type DownloadFile = { parts: DownloadFilePart[] downloadProgress?: DownloadProgressInfo }; - -export type DownloadEngineFileOptions = { - chunkSize?: number; - parallelStreams?: number; - retry?: retry.Options - comment?: string; - fetchStream: BaseDownloadEngineFetchStream, - writeStream: BaseDownloadEngineWriteStream -}; diff --git a/src/download/node-download.ts b/src/download/node-download.ts index 1e097ce..c710bf2 100644 --- a/src/download/node-download.ts +++ b/src/download/node-download.ts @@ -1,15 +1,37 @@ import DownloadEngineNodejs, {DownloadEngineOptionsNodejs} from "./download-engine/engine/download-engine-nodejs.js"; -import TransferCli from "./transfer-visualize/transfer-cli.js"; +import TransferCli, {CustomOutput, TransferCliOptions} from "./transfer-visualize/transfer-cli/transfer-cli.js"; import BaseDownloadEngine from "./download-engine/engine/base-download-engine.js"; import DownloadEngineMultiDownload from "./download-engine/engine/download-engine-multi-download.js"; +import transferCliSwitch, {AvailableTransferCli} from "./transfer-visualize/transfer-cli/transfer-cli-switch.js"; -export type DownloadEngineOptionsCLI = DownloadEngineOptionsNodejs & { + +export type CliProgressDownloadEngineOptions = { truncateName?: boolean | number; - cliProgress?: boolean | TransferCli; + cliProgress?: boolean | CustomOutput; + cliStyle?: AvailableTransferCli; cliName?: string; cliAction?: string; }; +export type DownloadEngineOptionsCLI = DownloadEngineOptionsNodejs & CliProgressDownloadEngineOptions; + +function createCliProgressForDownloadEngine(options: CliProgressDownloadEngineOptions) { + const cliOptions: TransferCliOptions = {...options}; + + if (options.cliAction) { + cliOptions.action = options.cliAction; + } + if (options.cliName) { + cliOptions.name = options.cliName; + } + + if (typeof options.cliProgress === "function") { + return TransferCli.customFormat(options.cliProgress); + } + + return transferCliSwitch(options.cliStyle, cliOptions); +} + /** * Download one file with CLI progress */ @@ -19,11 +41,7 @@ export async function downloadFile(options: DownloadEngineOptionsCLI) { if (options.cliProgress) { options.cliAction ??= options.fetchStrategy === "localFile" ? "Copying" : "Downloading"; - const cli = options.cliProgress instanceof TransferCli ? options.cliProgress : new TransferCli({ - ...options, - action: options.cliAction, - name: options.cliName - }); + const cli = createCliProgressForDownloadEngine(options); downloader.on("progress", cli.updateProgress); } @@ -31,11 +49,7 @@ export async function downloadFile(options: DownloadEngineOptionsCLI) { return downloader; } -export type DownloadSequenceOptions = { - truncateName?: boolean | number; - cliProgress?: boolean | TransferCli; - cliName?: string; - cliAction?: string; +export type DownloadSequenceOptions = CliProgressDownloadEngineOptions & { fetchStrategy?: "localFile" | "fetch"; }; @@ -56,16 +70,9 @@ export async function downloadSequence(options: DownloadSequenceOptions | Downlo if (downloadOptions.cliProgress) { if (downloadOptions.fetchStrategy) { downloadOptions.cliAction ??= downloadOptions.fetchStrategy === "localFile" ? "Copying" : "Downloading"; - } else { - downloadOptions.cliAction ??= "Transferring"; } - const cli = downloadOptions.cliProgress instanceof TransferCli ? downloadOptions.cliProgress : new TransferCli({ - ...options, - action: downloadOptions.cliAction, - name: downloadOptions.cliName - }); - + const cli = createCliProgressForDownloadEngine(downloadOptions); oneDownloader.on("progress", progress => { cli.updateProgress({ ...progress, diff --git a/src/download/transfer-visualize/progress-statistics-builder.ts b/src/download/transfer-visualize/progress-statistics-builder.ts index 48050c9..2e41663 100644 --- a/src/download/transfer-visualize/progress-statistics-builder.ts +++ b/src/download/transfer-visualize/progress-statistics-builder.ts @@ -1,18 +1,25 @@ import BaseDownloadEngine from "../download-engine/engine/base-download-engine.js"; -import Emittery from "emittery"; +import {EventEmitter} from "eventemitter3"; import TransferStatistics, {TransferProgressInfo} from "./transfer-statistics.js"; -import ProgressStatusFile from "../download-engine/progress-status-file.js"; import DownloadEngineFile from "../download-engine/download-engine-file.js"; import DownloadEngineMultiDownload from "../download-engine/engine/download-engine-multi-download.js"; -export type TransferProgressWithStatus = Omit & TransferProgressInfo & { index: number }; +export type TransferProgressWithStatus = TransferProgressInfo & { + totalBytes: number, + totalDownloadParts: number, + fileName: string, + comment?: string, + downloadPart: number, + bytesDownloaded: number, + index: number +}; interface CliProgressBuilderEvents { - progress: TransferProgressWithStatus; + progress: (progress: TransferProgressWithStatus) => void; } export type AnyEngine = DownloadEngineFile | BaseDownloadEngine | DownloadEngineMultiDownload; -export default class ProgressStatisticsBuilder extends Emittery { +export default class ProgressStatisticsBuilder extends EventEmitter { protected _engines: AnyEngine[] = []; protected _activeTransfers: { [index: number]: number } = {}; protected _totalBytes = 0; diff --git a/src/download/transfer-visualize/transfer-cli/transfer-cli-switch.ts b/src/download/transfer-visualize/transfer-cli/transfer-cli-switch.ts new file mode 100644 index 0000000..0cf1ced --- /dev/null +++ b/src/download/transfer-visualize/transfer-cli/transfer-cli-switch.ts @@ -0,0 +1,12 @@ +import TransferCli, {TransferCliOptions} from "./transfer-cli.js"; + +export type AvailableTransferCli = "simple" | "center-progress"; +export default function transferCliSwitch(name?: AvailableTransferCli, options?: T): TransferCli { + + switch (name) { + case "center-progress": + return null!; + } + + return new TransferCli(options); +} diff --git a/src/download/transfer-visualize/transfer-cli.ts b/src/download/transfer-visualize/transfer-cli/transfer-cli.ts similarity index 73% rename from src/download/transfer-visualize/transfer-cli.ts rename to src/download/transfer-visualize/transfer-cli/transfer-cli.ts index ac968d6..2cba271 100644 --- a/src/download/transfer-visualize/transfer-cli.ts +++ b/src/download/transfer-visualize/transfer-cli/transfer-cli.ts @@ -1,9 +1,9 @@ import logUpdate from "log-update"; import chalk from "chalk"; import debounce from "lodash.debounce"; -import {TRUNCATE_TEXT_MAX_LENGTH, truncateText} from "../../utils/truncate-text.js"; -import {clamp} from "../../utils/numbers.js"; -import {TransferProgressInfo} from "./transfer-statistics.js"; +import {TRUNCATE_TEXT_MAX_LENGTH, truncateText} from "../../../utils/truncate-text.js"; +import {clamp} from "../../../utils/numbers.js"; +import {TransferProgressInfo} from "../transfer-statistics.js"; import prettyBytes, {Options as PrettyBytesOptions} from "pretty-bytes"; import prettyMilliseconds, {Options as PrettyMsOptions} from "pretty-ms"; @@ -12,6 +12,16 @@ export type TransferCliStatus = TransferProgressInfo & { comment?: string }; +export type FormattedCliStatus = { + formattedSpeed: string, + transferredBytes: string, + formatTimeLeft: string, + formattedPercentage: string, + fileName?: string, + comment?: string +}; + + export type TransferCliOptions = { action?: string, name?: string, @@ -20,6 +30,8 @@ export type TransferCliOptions = { maxDebounceWait?: number; }; +export type CustomOutput = (info: TransferCliStatus & FormattedCliStatus) => string; + export const DEFAULT_TRANSFER_CLI_OPTIONS: TransferCliOptions = { action: "Transferring", truncateName: true, @@ -56,11 +68,15 @@ export default class TransferCli { ended: false }; - get formattedStatus() { + get currentStatus() { + return this._currentStatus; + } + + get currentFormattedStatus(): FormattedCliStatus { const formattedSpeed = TransferCli._formatSpeed(this._currentStatus.speed); const transferredBytes = `${prettyBytes(this._currentStatus.transferred, TransferCli._PRETTY_BYTES_OPTIONS)}/${prettyBytes(this._currentStatus.total, TransferCli._PRETTY_BYTES_OPTIONS)}`; const formatTimeLeft = prettyMilliseconds(this._currentStatus.timeLeft, TransferCli._PRETTY_MS_OPTIONS); - const percentageFormatted = this._currentStatus.percentage.toLocaleString(undefined, { + const formattedPercentage = this._currentStatus.percentage.toLocaleString(undefined, { ...TransferCli._NUMBER_FORMAT_OPTIONS, minimumIntegerDigits: 1 }) + "%"; @@ -69,7 +85,7 @@ export default class TransferCli { formattedSpeed, transferredBytes, formatTimeLeft, - percentageFormatted, + formattedPercentage, fileName: this._options.name || this._currentStatus.fileName, comment: this._currentStatus.comment ? `(${this._currentStatus.comment})` : "" }; @@ -90,7 +106,7 @@ export default class TransferCli { ...status }; this._currentStatus.fileName = this._truncateName(this._currentStatus.fileName); - this._logUpdate(this._createProgressBarFormat()); + this._logUpdate(this.createProgressBarFormat()); } protected _truncateName(text?: string) { @@ -101,7 +117,7 @@ export default class TransferCli { return text; } - protected _createProgressBarLine() { + protected createProgressBarLine() { const percentage = clamp(this._currentStatus.transferred / this._currentStatus.total, 0, 1); const fullLength = Math.floor(percentage * TransferCli.PROGRESS_BAR_LENGTH); const emptyLength = TransferCli.PROGRESS_BAR_LENGTH - fullLength; @@ -109,12 +125,12 @@ export default class TransferCli { return `${"=".repeat(fullLength)}>${" ".repeat(emptyLength)}`; } - protected _createProgressBarFormat(): string { - const {fileName, comment, formattedSpeed, transferredBytes, formatTimeLeft, percentageFormatted} = this.formattedStatus; + protected createProgressBarFormat(): string { + const {fileName, comment, formattedSpeed, transferredBytes, formatTimeLeft, formattedPercentage} = this.currentFormattedStatus; return `${chalk.cyan(this._options.action)} ${fileName} ${chalk.dim(comment)} -${chalk.green(percentageFormatted.padEnd(7)) - .padStart(6)} [${chalk.cyan(this._createProgressBarLine())}] ${TransferCli.centerPad(transferredBytes, 18)} ${TransferCli.centerPad(formattedSpeed, 10)} ${TransferCli.centerPad(formatTimeLeft, 5)} left`; +${chalk.green(formattedPercentage.padEnd(7)) + .padStart(6)} [${chalk.cyan(this.createProgressBarLine())}] ${TransferCli.centerPad(transferredBytes, 18)} ${TransferCli.centerPad(formattedSpeed, 10)} ${TransferCli.centerPad(formatTimeLeft, 5)} left`; } protected _logUpdate(text: string) { @@ -131,4 +147,19 @@ ${chalk.green(percentageFormatted.padEnd(7)) private static _formatSpeed(speed: number): string { return prettyBytes(Math.min(speed, 9999999999) || 0, TransferCli._PRETTY_BYTES_OPTIONS) + "/s"; } + + /** + * Create a custom format for the progress bar. + */ + static customFormat(callback: CustomOutput, options?: TransferCliOptions) { + const transferCli = new TransferCli(options); + transferCli.createProgressBarFormat = () => { + return callback({ + ...transferCli.currentStatus, + ...transferCli.currentFormattedStatus + }); + }; + + return transferCli; + } } diff --git a/src/index.ts b/src/index.ts index 74909bd..11eb260 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ import {truncateText} from "./utils/truncate-text.js"; import TransferStatistics, {TransferProgressInfo} from "./download/transfer-visualize/transfer-statistics.js"; -import TransferCli, {TransferCliOptions, TransferCliStatus} from "./download/transfer-visualize/transfer-cli.js"; +import TransferCli, {TransferCliOptions, TransferCliStatus} from "./download/transfer-visualize/transfer-cli/transfer-cli.js"; import DownloadEngineNodejs, {DownloadEngineOptionsNodejs} from "./download/download-engine/engine/download-engine-nodejs.js"; import DownloadEngineFile, {DownloadEngineFileOptionsWithDefaults} from "./download/download-engine/download-engine-file.js"; import {downloadFile, downloadSequence} from "./download/node-download.js";