@fjedi/nodejs-file-downloader
Version:
A file downloader for NodeJs
208 lines (189 loc) • 5.37 kB
JavaScript
const rpur = require("./utils/rpur");
const { capitalize } = require("./utils/string");
const Download = require("./Download");
const configTypes = {
url: {
type: "string",
mandatory: true,
},
directory: {
type: "string",
mandatory: false,
},
fileName: {
type: "string",
mandatory: false,
},
cloneFiles: {
type: "boolean",
mandatory: false,
},
proxy: {
type: "string",
mandatory: false,
},
};
const validateConfig = (config) => {
const generateTypeError = (prop) => {
throw new Error(`config.${prop} must be of type ${configTypes[prop].type}`);
};
for (let prop in configTypes) {
if (configTypes[prop].mandatory) {
if (!config[prop]) throw new Error(`Must supply a config.${prop}`);
if (-1 === [].concat(configTypes[prop].type).indexOf(typeof config[prop]))
generateTypeError(prop);
}
if (
config.hasOwnProperty(prop) &&
-1 === [].concat(configTypes[prop].type).indexOf(typeof config[prop])
)
generateTypeError(prop);
}
};
module.exports = class Downloader {
/**
*
* @param {object} config
* @param {string} config.url
* @param {string} [config.directory]
* @param {string} [config.fileName = undefined]
* @param {boolean} [config.cloneFiles=true] true will create a duplicate. false will overwrite the existing file.
* @param {boolean} [config.skipExistingFileName = false] true will cause the downloader to skip the download process in case a file with the same name already exists.
* @param {number} [config.timeout=6000]
* @param {number} [config.maxAttempts=1]
* @param {number} [config.delayBetweenAttempts=3000]
* @param {object} [config.headers = undefined]
* @param {object} [config.httpsAgent = undefined]
* @param {string} [config.proxy = undefined]
* @param {function} [config.onError = undefined]
* @param {function} [config.onResponse = undefined]
* @param {function} [config.onBeforeSave = undefined]
* @param {function} [config.onProgress = undefined]
* @param {function} [config.shouldStop = undefined]
* @param {boolean} [config.shouldBufferResponse = false]
* @param {boolean} [config.useSynchronousMode = false]
*/
constructor(config) {
// super();
if (!config || typeof config !== "object") {
throw new Error("Must provide a valid config object");
}
validateConfig(config);
const defaultConfig = {
directory: "./",
fileName: undefined,
timeout: 6000,
maxAttempts: 1,
delayBetweenAttempts: 3000,
useSynchronousMode: false,
httpsAgent: undefined,
proxy: undefined,
headers: undefined,
cloneFiles: true,
skipExistingFileName: false,
shouldBufferResponse: false,
onAttempt: undefined,
onResponse: undefined,
onBeforeSave: undefined,
onError: undefined,
onProgress: undefined,
};
this.config = {
...defaultConfig,
...config,
};
if (this.config.filename) {
this.config.fileName = this.config.filename;
}
this._currentDownload = null;
}
//For EventEmitter backwards compatibility
on(event, callback) {
this.config[`on${capitalize(event)}`] = callback;
}
/**
* @return {Promise<{filePath:string|null,downloadStatus:"ABORTED"|"COMPLETE"}>}
*/
async download() {
const that = this;
const {
url,
directory,
fileName,
cloneFiles,
skipExistingFileName,
timeout,
headers,
httpsAgent,
proxy,
onResponse,
onBeforeSave,
onProgress,
shouldBufferResponse,
useSynchronousMode,
} = that.config;
// Repeat downloading process until success
const { filePath, downloadStatus } = await that._makeUntilSuccessful(
async () => {
const download = new Download({
url,
directory,
fileName,
cloneFiles,
skipExistingFileName,
timeout,
headers,
httpsAgent,
proxy,
onResponse,
onBeforeSave,
onProgress,
shouldBufferResponse,
useSynchronousMode,
});
this._currentDownload = download;
return await download.start();
}
);
return { filePath, downloadStatus };
debugger;
}
/**
* @param {Function} asyncFunc
* @return {Promise<any>}
*/
async _makeUntilSuccessful(asyncFunc) {
let data;
const func = asyncFunc.bind(this);
await rpur(
async () => {
data = await func();
},
{
onError: async (e) => {
if (this.config.onError) {
await this.config.onError(e);
}
},
shouldStop: async (e) => {
if (e.code === "ERR_REQUEST_CANCELLED")
// Means the request was cancelled, therefore no repetition is required.
return true;
if (this.config.shouldStop) {
if ((await this.config.shouldStop(e)) === true) {
return true;
}
}
},
timeout: this.config.timeout,
maxAttempts: this.config.maxAttempts,
delay: this.config.delayBetweenAttempts,
onAttempt: this.config.onAttempt,
}
);
return data;
}
cancel() {
this._currentDownload.cancel();
}
};