Source: index.js

/**
 * Module for download file (use stream methods)
 * @module receive-file
 * @author Siarhei Dudko <slavianich@gmail.com>
 * @copyright 2019-2020
 * @license MIT
 * @version 2.0.1
 * @requires fs
 * @requires url
 * @requires http
 * @requires https
 * @requires path
 */

'use strict'

let Fs = require('fs'),
	Url = require('url'),
	Http = require('http'),
	Https = require('https'),
	Path = require('path');
  
let nodeVers = process.version.substr(1);

/**
  * Compare version function
  * 
  * @private
  * @function
  * @param {string} v1 - first version
  * @param {string} v2 - second version
  * @return {number} n - if v1 > v2 return 1, if v1 < v2 return 0, else return -1
  */
function compareVers(v1, v2){
	if((typeof(v1) !== 'string') 
		|| (typeof(v2) !== 'string')
	){
		return new Error('The arguments passed are not string values!');
	}
	const _v1 = v1.match(/^(\d+\.){0,3}(\d+)/gi);
	const _v2 = v2.match(/^(\d+\.){0,3}(\d+)/gi);
	if(!(Array.isArray(_v1) && (typeof(_v1[0]) === 'string')) 
		|| !(Array.isArray(_v2) && (typeof(_v2[0]) === 'string'))
	){
		return new Error('The arguments passed are not string versions!');
	}
	try{
		const _vn1 = _v1[0].split('.').map(function(arg){ return Number.parseInt(arg, 10); });
		const _vn2 = _v2[0].split('.').map(function(arg){ return Number.parseInt(arg, 10); });
		for(let i = 0; i < _vn1.length; i++){
			if(_vn1[i] > _vn2[i])
				return 1;
			if(_vn1[i] < _vn2[i])
				return -1;
		}
		return 0;
	} catch(err){
		return new Error('Version comparison error:'+err.message);
	}
}

/**
  * Recursive make dir promise
  * 
  * @private
  * @async
  * @function
  * @param {string} path - path to create directories
  * @param {RecursiveMkdirSettings} options - settings to create directories
  * @return {Promise} promise
  */
let promiseMkdir = function(path, obj = {recursive: false, mode: 0o777}){
	if(compareVers(nodeVers, '10.0.0') >= 0){
		return Fs.promises.mkdir(path, obj);
	} else {
		return new Promise((res, rej) => {
			recursiveMkdir(path, obj, function(err){
				if(err){
					rej(err);
				} else {
					res();
				}
			});
		});
	}
}

/**
  * Recursive make dir function
  * 
  * @private
  * @function
  * @param {string} path - path to create directories
  * @param {RecursiveMkdirSettings} options - settings to create directories
  * @param {RecursiveMkdirCallback} callback - callback function
  */
let recursiveMkdir = function(path, obj, callback){
	let _path;
	if(obj.recursive === true){
		_path = path.split(Path.sep);
		while(_path[0] === ''){
			_path = _path.splice(1);
		}
	} else {
		_path = [path];
	}
	try{
		Fs.mkdir(_path[0], {mode: obj.mode}, (err) => {
			if(err && (err.code !== 'EEXIST')) {
				callback(err);
			} else {
				_path = _path.splice(1);
				while(_path[0] === ''){
					_path = _path.splice(1);
				}
				if((_path.length !== 0) && (obj.recursive === true)){
					path = Path.join(_path);
					recursiveMkdir(path, obj, callback);
				} else {
					callback();
				}
			}
		});
	} catch(err){
		callback(err);
	}
}

/**
  * File delete function
  * 
  * @private
  * @async
  * @function
  * @param {string} file - path to delete file
  * @param {Error} msg - Error instance to send to the callback function
  * @param {DeleteFileCallback} callback
  */
let deleteFile = function(file, msg, callback){
	Fs.unlink(file, () => {
		callback(msg);
	});
}
/**
  * File download promise
  * 
  * @private
  * @async
  * @function
  * @param {string} url - file download link
  * @param {ReceiveFileSettings} options - download settings object
  * @param {number} level - iteration number, to avoid cyclic redirect
  * @return {Promise<string>} path - path where the file was downloaded
  */
let receivefile = function(url, options, level = 0){
	let dt = Date.now();
	return new Promise((res, rej) => {
		let path = Path.join(options.directory, options.filename);
		let Req;
		let Options = Url.parse(url);
		if (Options.protocol === 'https:') {
			Req = Https;
		} else {
			Req = Http;
		}
		let request = Req.request(Options,(response) => {
			switch(response.statusCode){
				case 200:
				case 201:
				case 202:
				case 203:
				case 204:
				case 205:
					promiseMkdir(options.directory, {recursive: true, mode: 0o666}).then(() => {
						let filestream = Fs.createWriteStream(path);
						let _timer = options.timeout + dt;
						response.on('data', () => {
							if((_timer - Date.now()) < 0) { request.abort(); }
						}).pipe(filestream);
						filestream.on("finish",() => {
							if((typeof(response.headers['content-length']) === 'string') && (response.headers['content-length'] !== '')){
								Fs.stat(path, (err, stat) => {
									if(err){
										deleteFile(path, err, rej);
									} else if ((typeof(stat) === 'object') && stat.isFile()) {
										if(stat.size.toString() !== response.headers['content-length']){
											deleteFile(path, new Error('File not full!'), rej);
										} else {
											res(path);
										}
									} else {
										deleteFile(path, new Error('Not Found'), rej);
									}
								});
							} else {
								res(path);
							} 
						});
						filestream.on("error",(err) => {
							deleteFile(path, err, rej);
						});
						response.on('aborted', () => {
							response.unpipe(filestream);
							response.destroy();
							filestream.destroy(new Error('Request aborted!'));
						});
					}).catch(rej);
					break;
				case 300:
				case 301:
				case 302:
				case 303:
				case 304:
				case 305:
				case 306:
				case 307:
				case 308:
					if((typeof(response.headers['location']) === 'string') && (level < 3)){
						receivefile(response.headers['location'], options, ++level).then(res).catch(rej);
					} else {
						rej(new Error(response.statusCode + ' - ' + response.statusMessage));
					}
					break;
				default:
					rej(new Error(response.statusCode + ' - ' + response.statusMessage));
					break;
			}
		}).on('timeout', () => {
			request.abort();
		}).on('error', (err) => {
			rej(err);
		});
		let timeout = options.timeout - (Date.now() - dt );
		if(timeout > 0){
			request.setTimeout(timeout);
			request.end();
		} else {
			request.abort();
			rej(new Error('Request aborted with timeout!'));
		}
	});
}

/**
  * File download function
  * 
  * @async
  * @function
  * @param {string} url - file download link
  * @param {ReceiveFileSettings} options - download settings object
  * @param {ReceiveFileCallback} callback - if not set, promise will be returned
  * @return {Promise<string>} path - path where the file was downloaded
  */
let ReceiveFile = function(url, options, callback){
	if ((typeof(callback) !== 'function') && (typeof(options) === 'function')) {
		callback = options;
	}
	if(typeof(options) !== 'object'){
		options = {};
	}
	if(!Number.isInteger(options.timeout)){
		options.timeout = 30000;
	}
	if(typeof(options.directory) !== 'string'){
		options.directory = Path.normalize('.');
	} else {
		options.directory = Path.normalize(options.directory);
	}
	if(typeof(options.filename) !== 'string'){
		let arr = url.split('/');
		options.filename = arr[arr.length - 1];
	}
	if(typeof(url) !== 'string'){
		if(typeof(callback) !== 'function') {
			return Promise.reject(new Error("Need a file url to download"));
		} else {
			callback(new Error("Need a file url to download"));
			return;
		}
	} else if(typeof(callback) !== 'function') {
		return receivefile(url, options, 0); 
	} else {
		receivefile(url, options, 0).then((_path) => { callback(undefined, _path); }).catch((err) => { callback(err); });
		return;
	}
};

/**
 * ReceiveFile Callback Function, if not set, promise will be returned
 *
 * @callback ReceiveFileCallback
 * @param {Error} err - if the request completed with an error, or undefined
 * @param {string} path - if the request is successful, or undefined
 */
 
 /**
 * ReceiveFile Object Settings
 *
 * @namespace ReceiveFileSettings
 * @property {string} directory - directory for download file
 * @property {string} filename - filename for download file
 * @property {number} timeout - download file timeout, milliseconds
 */
 
 /**
 * Delete File Callback Function
 *
 * @private
 * @callback DeleteFileCallback
 * @param {Error} err - instance of Error
 */
 
  /**
 * RecursiveMkdir Object Settings
 *
 * @private
 * @namespace RecursiveMkdirSettings
 * @property {boolean} recursive - Default: false
 * @property {number} mode - Not supported on Windows. Default: 0o777.
 */
 
 /**
 * RecursiveMkdir Callback Function
 *
 * @private
 * @callback RecursiveMkdirCallback
 * @param {Error} err - instance of Error
 */

module.exports = ReceiveFile;