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.
157 lines • 7.08 kB
JavaScript
import path from "path";
import DownloadEngineFile from "../download-file/download-engine-file.js";
import DownloadEngineFetchStreamFetch from "../streams/download-engine-fetch-stream/download-engine-fetch-stream-fetch.js";
import DownloadEngineWriteStreamNodejs from "../streams/download-engine-write-stream/download-engine-write-stream-nodejs.js";
import DownloadEngineFetchStreamLocalFile from "../streams/download-engine-fetch-stream/download-engine-fetch-stream-local-file.js";
import BaseDownloadEngine from "./base-download-engine.js";
import SavePathError from "./error/save-path-error.js";
import fs from "fs-extra";
import filenamify from "filenamify";
import { DownloadStatus } from "../download-file/progress-status-file.js";
export const PROGRESS_FILE_EXTENSION = ".ipull";
/**
* Download engine for Node.js
*/
export default class DownloadEngineNodejs extends BaseDownloadEngine {
options;
constructor(engine, _options) {
super(engine, _options);
this.options = _options;
}
_initEvents() {
super._initEvents();
this._engine.options.onSaveProgressAsync = async (progress) => {
if (this.options.skipExisting)
return;
await this.options.writeStream.saveMetadataAfterFile(progress);
};
this._engine.options.onPausedAsync = async () => {
await this.options.writeStream.ensureBytesSynced();
};
// Try to clone the file if it's a single part download
this._engine.options.onStartedAsync = async () => {
if (this.options.skipExisting || this.options.fetchStrategy !== "local" || this.options.partURLs.length !== 1)
return;
try {
const { reflinkFile } = await import("@reflink/reflink");
await fs.remove(this.options.writeStream.path);
await reflinkFile(this.options.partURLs[0], this.options.writeStream.path);
this._engine.finished("cloned");
}
catch { }
};
this._engine.options.onFinishAsync = async () => {
if (this.options.skipExisting)
return;
await this.options.writeStream.ftruncate(this.downloadSize);
};
this._engine.options.onCloseAsync = async () => {
if (this.status.ended && this.options.writeStream.path != this.options.writeStream.finalPath) {
await fs.rename(this.options.writeStream.path, this.options.writeStream.finalPath);
this.options.writeStream.path = this.options.writeStream.finalPath;
}
};
if (this.options.skipExisting) {
this.options.writeStream.path = this.options.writeStream.finalPath;
}
}
/**
* The file path with the progress extension or the final file path if the download is finished
*/
get fileAbsolutePath() {
return path.resolve(this.options.writeStream.path);
}
/**
* The final file path (without the progress extension)
*/
get finalFileAbsolutePath() {
return path.resolve(this.options.writeStream.finalPath);
}
/**
* Abort the download & delete the file (**even if** the download is finished)
* @deprecated use `close` with flag `deleteFile` instead
*
* TODO: remove in the next major version
*/
async closeAndDeleteFile() {
await this.close({ deleteFile: true });
}
/**
* Close the download engine
* @param deleteTempFile {boolean} - delete the temp file (when the download is **not finished**).
* @param deleteFile {boolean} - delete the **temp** or **final file** (clean everything up).
*/
async close({ deleteTempFile, deleteFile } = {}) {
await super.close();
if (deleteFile || deleteTempFile && this.status.downloadStatus != DownloadStatus.Finished) {
try {
await fs.unlink(this.fileAbsolutePath);
}
catch { }
}
}
/**
* Download/copy a file
*
* if `fetchStrategy` is defined as "localFile" it will copy the file, otherwise it will download it
* By default, it will guess the strategy based on the URL
*/
static async createFromOptions(options) {
DownloadEngineNodejs._validateOptions(options);
const partURLs = "partURLs" in options ? options.partURLs : [options.url];
options.fetchStrategy ??= DownloadEngineNodejs._guessFetchStrategy(partURLs[0]);
const fetchStream = options.fetchStrategy === "local" ?
new DownloadEngineFetchStreamLocalFile(options) :
new DownloadEngineFetchStreamFetch(options);
return DownloadEngineNodejs._createFromOptionsWithCustomFetch({ ...options, partURLs, fetchStream });
}
static async _createFromOptionsWithCustomFetch(options) {
const downloadFile = await DownloadEngineNodejs._createDownloadFile(options.partURLs, options.fetchStream);
const downloadLocation = DownloadEngineNodejs._createDownloadLocation(downloadFile, options);
downloadFile.localFileName = path.basename(downloadLocation);
const writeStream = new DownloadEngineWriteStreamNodejs(downloadLocation + PROGRESS_FILE_EXTENSION, downloadLocation, options);
writeStream.fileSize = downloadFile.totalSize;
downloadFile.downloadProgress = await writeStream.loadMetadataAfterFileWithoutRetry();
if (options.skipExisting) {
options.skipExisting = false;
if (downloadFile.totalSize > 0 && !downloadFile.downloadProgress) {
try {
const stat = await fs.stat(downloadLocation);
if (stat.isFile() && stat.size === downloadFile.totalSize) {
options.skipExisting = true;
}
}
catch { }
}
}
const allOptions = { ...options, writeStream };
const engine = new DownloadEngineFile(downloadFile, allOptions);
return new DownloadEngineNodejs(engine, allOptions);
}
static _createDownloadLocation(download, options) {
if ("savePath" in options) {
return options.savePath;
}
const fileName = options.fileName || download.localFileName;
return path.join(options.directory, filenamify(fileName));
}
static _validateOptions(options) {
super._validateOptions(options);
if (!("directory" in options) && !("savePath" in options)) {
throw new SavePathError("Either `directory` or `savePath` must be provided");
}
if ("directory" in options && "savePath" in options) {
throw new SavePathError("Both `directory` and `savePath` cannot be provided");
}
DownloadEngineNodejs._validateURL(options);
}
static _guessFetchStrategy(url) {
try {
new URL(url);
return "remote";
}
catch { }
return "local";
}
}
//# sourceMappingURL=download-engine-nodejs.js.map