├── .gitignore ├── LICENSE ├── README.md ├── examples ├── _handleEvents.js ├── _printStats.js ├── custom-download-options.js ├── destroy-download.js ├── multiple-downloads.js ├── resume-download.js ├── simple-download.js └── stop-n-resume-download.js ├── lib ├── Download.js ├── Downloader.js └── Formatters.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | tmp/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Leeroy Brun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multi Threaded Files Downloader 2 | 3 | [![NPM](https://nodei.co/npm/mt-files-downloader.png)](https://nodei.co/npm/mt-files-downloader/) 4 | 5 | **NOT VERY STABLE**: This module is not very stable, and is in need of a big refactor to work with the latest `mt-downloader` version. I will try to do this when I have the time, but unfotrunately other projects are taking a lot of time from me right now. Thanks for your understanding. 6 | 7 | This module wrap the [mt-downloader](https://www.npmjs.com/package/mt-downloader) module and let you : 8 | 9 | - Manage multiple downloads 10 | - Get stats (speed, eta, completed, etc) 11 | - Auto-retry (continue) a download in case of error (ie. network error) 12 | - Manually resume a download from partial file 13 | - Stop and resume downloads 14 | - Get notified by events when a download start, fail, retry, stopped, destroyed or complete 15 | 16 | ## Install 17 | 18 | npm install mt-files-downloader 19 | 20 | ## Usage 21 | 22 | Require the module : 23 | 24 | var Downloader = require('mt-files-downloader'); 25 | 26 | Create a new Downloader instance : 27 | 28 | var downloader = new Downloader(); 29 | 30 | Create a new download : 31 | 32 | var dl = downloader.download('FILE_URL', 'FILE_SAVE_PATH'); 33 | 34 | Start the download : 35 | 36 | dl.start(); 37 | 38 | ## Examples 39 | 40 | You can find complete examples in the `examples/` folder : 41 | 42 | - [Simple download](https://github.com/leeroybrun/node-mt-files-downloader/blob/master/examples/simple-download.js) 43 | - [Multiple downloads](https://github.com/leeroybrun/node-mt-files-downloader/blob/master/examples/multiple-downloads.js) 44 | - [Resume download](https://github.com/leeroybrun/node-mt-files-downloader/blob/master/examples/resume-download.js) 45 | - [Stop & resume download](https://github.com/leeroybrun/node-mt-files-downloader/blob/master/examples/stop-n-resume-download.js) 46 | - [Destroy download](https://github.com/leeroybrun/node-mt-files-downloader/blob/master/examples/destroy-download.js) 47 | - [Custom download options](https://github.com/leeroybrun/node-mt-files-downloader/blob/master/examples/custom-download-options.js) 48 | 49 | ## Events 50 | 51 | You can then listen to those events : 52 | 53 | - `dl.on('start', function(dl) { ... });` 54 | - `dl.on('error', function(dl) { ... });` 55 | - `dl.on('end', function(dl) { ... });` 56 | - `dl.on('stopped', function(dl) { ... });` 57 | - `dl.on('destroyed', function(dl) { ... });` 58 | - `dl.on('retry', function(dl) { ... });` 59 | 60 | ## Downloader object 61 | 62 | ### Methods 63 | 64 | - download(URL, FILE_SAVE_PATH, [options]) 65 | - URL : URL of the file to download 66 | - FILE_SAVE_PATH : where to save the file (including filename !) 67 | - options : optional, passed directly to Download object 68 | - resumeDownload(filePath) : create a new download by resuming from an existing file 69 | - getDownloads() : get the list of downloads in manager 70 | - getDownloadByUrl(url) : get a specified download by URL 71 | - getDownloadByFilePath(filePath) : get a specified download by file path 72 | - removeDownloadByFilePath(filePath) : remove a specified download by file path. It does not destroy it, just remove from download manager ! Call download.destroy() before if you want to completely remove it. 73 | 74 | ### Formatters methods 75 | 76 | The Downloader object exposes some formatters for the stats as static methods : 77 | 78 | - Downloader.Formatters.speed(speed) 79 | - Downloader.Formatters.elapsedTime(seconds) 80 | - Downloader.Formatters.remainingTime(seconds) 81 | 82 | ## Download object 83 | 84 | ### Properties 85 | 86 | - status : 87 | - -3 = destroyed 88 | - -2 = stopped 89 | - -1 = error 90 | - 0 = not started 91 | - 1 = started (downloading) 92 | - 2 = error, retrying 93 | - 3 = finished 94 | - url 95 | - filePath 96 | - options 97 | - meta 98 | 99 | ### Methods 100 | 101 | - setUrl(url) : set the download URL 102 | - setFilePath(path) : set the download file save path 103 | - setOptions(options) : set the download options 104 | - threadsCount: Default: 2, Set the total number of download threads 105 | - method: Default: GET, HTTP method 106 | - port: Default: 80, HTTP port 107 | - timeout: Default: 5000, If no data is received, the download times out (milliseconds) 108 | - range: Default: 0-100, Control the part of file that needs to be downloaded. 109 | - setRetryOptions(options) : set the retry options 110 | - maxRetries: Default 5, max number of retries before considering the download as failed 111 | - retryInterval: Default 2000, interval (milliseconds) between each retry 112 | - setMeta(meta) : set download metadata 113 | - setStatus(status) : set download status 114 | - setError(error) : set error message for download 115 | - getStats() : compute and get stats for the download 116 | - start() : start download 117 | - resume() : resume download 118 | - stop() : stop the download, keep the files 119 | - destroy() : stop the download, remove files 120 | 121 | ## TODO 122 | 123 | - Validate data (setters) 124 | - Add tests 125 | 126 | ## Licence 127 | 128 | The MIT License (MIT) 129 | 130 | Copyright (c) 2015 Leeroy Brun 131 | 132 | Permission is hereby granted, free of charge, to any person obtaining a copy 133 | of this software and associated documentation files (the "Software"), to deal 134 | in the Software without restriction, including without limitation the rights 135 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 136 | copies of the Software, and to permit persons to whom the Software is 137 | furnished to do so, subject to the following conditions: 138 | 139 | The above copyright notice and this permission notice shall be included in all 140 | copies or substantial portions of the Software. 141 | 142 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 143 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 144 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 145 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 146 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 147 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 148 | SOFTWARE. 149 | -------------------------------------------------------------------------------- /examples/_handleEvents.js: -------------------------------------------------------------------------------- 1 | module.exports = function(dl, num) { 2 | num = num || 1; 3 | 4 | dl.on('start', function() { 5 | console.log('EVENT - Download '+ num +' started !'); 6 | }); 7 | 8 | dl.on('error', function() { 9 | console.log('EVENT - Download '+ num +' error !'); 10 | console.log(dl.error); 11 | }); 12 | 13 | dl.on('end', function() { 14 | console.log('EVENT - Download '+ num +' finished !'); 15 | 16 | console.log(dl.getStats()); 17 | }); 18 | 19 | dl.on('retry', function() { 20 | console.log('EVENT - Download '+ num +' error, retrying...'); 21 | }); 22 | 23 | dl.on('stopped', function() { 24 | console.log('EVENT - Download '+ num +' stopped...'); 25 | }); 26 | 27 | dl.on('destroyed', function() { 28 | console.log('EVENT - Download '+ num +' destroyed...'); 29 | }); 30 | }; -------------------------------------------------------------------------------- /examples/_printStats.js: -------------------------------------------------------------------------------- 1 | var Downloader = require('../lib/Downloader'); 2 | 3 | module.exports = function(dl, num) { 4 | num = num || 1; 5 | 6 | var timer = setInterval(function() { 7 | if(dl.status == 0) { 8 | console.log('Download '+ num +' not started.'); 9 | } else if(dl.status == 1) { 10 | var stats = dl.getStats(); 11 | console.log('Download '+ num +' is downloading:'); 12 | console.log('Download progress: '+ stats.total.completed +' %'); 13 | console.log('Download speed: '+ Downloader.Formatters.speed(stats.present.speed)); 14 | console.log('Download time: '+ Downloader.Formatters.elapsedTime(stats.present.time)); 15 | console.log('Download ETA: '+ Downloader.Formatters.remainingTime(stats.future.eta)); 16 | } else if(dl.status == 2) { 17 | console.log('Download '+ num +' error... retrying'); 18 | } else if(dl.status == 3) { 19 | console.log('Download '+ num +' completed !'); 20 | } else if(dl.status == -1) { 21 | console.log('Download '+ num +' error : '+ dl.error); 22 | } else if(dl.status == -2) { 23 | console.log('Download '+ num +' stopped.'); 24 | } else if(dl.status == -3) { 25 | console.log('Download '+ num +' destroyed.'); 26 | } 27 | 28 | console.log('------------------------------------------------'); 29 | 30 | if(dl.status === -1 || dl.status === 3 || dl.status === -3) { 31 | clearInterval(timer); 32 | timer = null; 33 | } 34 | }, 1000); 35 | }; 36 | -------------------------------------------------------------------------------- /examples/custom-download-options.js: -------------------------------------------------------------------------------- 1 | var os = require('os'); 2 | var path = require('path'); 3 | var Downloader = require('../lib/Downloader'); 4 | 5 | // Create new downloader 6 | var downloader = new Downloader(); 7 | 8 | var fileUrl = 'http://ipv4.download.thinkbroadband.com/20MB.zip'; 9 | var fileSavePath = path.join(os.tmpdir(), 'mtFileDlTest1.zip'); 10 | console.log('File will be downloaded from '+ fileUrl +' to '+ fileSavePath); 11 | 12 | // Start download 13 | var dl = downloader.download(fileUrl, fileSavePath); 14 | 15 | // Set retry options 16 | dl.setRetryOptions({ 17 | maxRetries: 3, // Default: 5 18 | retryInterval: 1000 // Default: 2000 19 | }); 20 | 21 | // Set download options 22 | dl.setOptions({ 23 | threadsCount: 5, // Default: 2, Set the total number of download threads 24 | method: 'GET', // Default: GET, HTTP method 25 | port: 80, // Default: 80, HTTP port 26 | timeout: 5000, // Default: 5000, If no data is received, the download times out (milliseconds) 27 | range: '0-100', // Default: 0-100, Control the part of file that needs to be downloaded. 28 | }); 29 | 30 | // Import generic examples for handling events and printing stats 31 | require('./_handleEvents')(dl); 32 | require('./_printStats')(dl); 33 | 34 | dl.start(); 35 | 36 | dl.on('start', function() { 37 | console.log('Download started with '+ ((dl.meta.threads) ? dl.meta.threads.length : 0) +' threads.') 38 | }); -------------------------------------------------------------------------------- /examples/destroy-download.js: -------------------------------------------------------------------------------- 1 | var os = require('os'); 2 | var path = require('path'); 3 | 4 | var Downloader = require('../lib/Downloader'); 5 | 6 | var downloader = new Downloader(); 7 | 8 | var fileUrl = 'http://ipv4.download.thinkbroadband.com/100MB.zip'; 9 | var fileSavePath = path.join(os.tmpdir(), 'mtFileDlTest1.zip'); 10 | 11 | console.log('File will be downloaded from '+ fileUrl +' to '+ fileSavePath); 12 | 13 | var dl = downloader.download(fileUrl, fileSavePath) 14 | .start(); 15 | 16 | require('./_handleEvents')(dl); 17 | require('./_printStats')(dl); 18 | 19 | // Wait 10s before destroying download 20 | setTimeout(function() { 21 | console.log('DEMO - Destroying download...'); 22 | 23 | dl.destroy(); 24 | 25 | console.log('Downloads in manager: '+ downloader.getDownloads().length); 26 | 27 | if(downloader.removeDownloadByFilePath(dl.filePath)) { 28 | console.log('Download removed from manager !'); 29 | } else { 30 | console.log('Error when trying to remove download from manager !'); 31 | } 32 | 33 | console.log('Downloads in manager: '+ downloader.getDownloads().length); 34 | }, 10000); 35 | 36 | -------------------------------------------------------------------------------- /examples/multiple-downloads.js: -------------------------------------------------------------------------------- 1 | var Downloader = require('../lib/Downloader'); 2 | var path = require('path'); 3 | var os = require('os'); 4 | 5 | var handleEvents = require('./_handleEvents'); 6 | var printStats = require('./_printStats'); 7 | 8 | var registerDlEvents = function(num, dl) { 9 | handleEvents(dl, num); 10 | printStats(dl, num); 11 | }; 12 | 13 | var downloader = new Downloader(); 14 | 15 | var fileUrl1 = 'http://ipv4.download.thinkbroadband.com/200MB.zip'; 16 | var fileSavePath1 = path.join(os.tmpdir(), 'mtFileDlTest1.zip'); 17 | var fileUrl2 = 'http://ipv4.download.thinkbroadband.com/100MB.zip'; 18 | var fileSavePath2 = path.join(os.tmpdir(), 'mtFileDlTest2.zip'); 19 | 20 | console.log('First file will be downloaded from '+ fileUrl1 +' to '+ fileSavePath1); 21 | console.log('Second file will be downloaded from '+ fileUrl2 +' to '+ fileSavePath2); 22 | 23 | var dl1 = downloader.download(fileUrl1, fileSavePath1) 24 | .start(); 25 | 26 | var dl2 = downloader.download(fileUrl2, fileSavePath2) 27 | .start(); 28 | 29 | registerDlEvents(1, dl1); 30 | registerDlEvents(2, dl2); 31 | 32 | -------------------------------------------------------------------------------- /examples/resume-download.js: -------------------------------------------------------------------------------- 1 | var os = require('os'); 2 | var path = require('path'); 3 | 4 | var Downloader = require('../lib/Downloader'); 5 | 6 | var downloader = new Downloader(); 7 | 8 | var fileSavePath = path.join(os.tmpdir(), 'mtFileDlTest1.zip'); 9 | 10 | console.log('File will be resumed from '+ fileSavePath); 11 | 12 | var dl = downloader.resumeDownload(fileSavePath); 13 | 14 | require('./_handleEvents')(dl); 15 | require('./_printStats')(dl); 16 | 17 | dl.on('error', function() { 18 | if((''+dl.error).indexOf('Invalid file path') !== -1) { 19 | console.log('We cannot resume the download if the tmp file does not exists.'); 20 | console.log('Before calling this example, you should :'); 21 | console.log(' 1. call simple-download.js'); 22 | console.log(' 2. wait for it to download a little bit of the file'); 23 | console.log(' 3. cancel by pressing Ctrl+C'); 24 | console.log(' 4. call resume-download.js again'); 25 | } 26 | }); 27 | 28 | dl.start(); -------------------------------------------------------------------------------- /examples/simple-download.js: -------------------------------------------------------------------------------- 1 | var os = require('os'); 2 | var path = require('path'); 3 | var Downloader = require('../lib/Downloader'); 4 | 5 | // Create new downloader 6 | var downloader = new Downloader(); 7 | 8 | var fileUrl = 'http://ipv4.download.thinkbroadband.com/20MB.zip'; 9 | var fileSavePath = path.join(os.tmpdir(), 'mtFileDlTest1.zip'); 10 | console.log('File will be downloaded from '+ fileUrl +' to '+ fileSavePath); 11 | 12 | // Start download 13 | var dl = downloader.download(fileUrl, fileSavePath) 14 | .start(); 15 | 16 | // Import generic examples for handling events and printing stats 17 | require('./_handleEvents')(dl); 18 | require('./_printStats')(dl); -------------------------------------------------------------------------------- /examples/stop-n-resume-download.js: -------------------------------------------------------------------------------- 1 | var os = require('os'); 2 | var path = require('path'); 3 | var Downloader = require('../lib/Downloader'); 4 | 5 | // Create new downloader 6 | var downloader = new Downloader(); 7 | 8 | var fileUrl = 'http://ipv4.download.thinkbroadband.com/100MB.zip'; 9 | var fileSavePath = path.join(os.tmpdir(), 'mtFileDlTest1.zip'); 10 | console.log('File will be downloaded from '+ fileUrl +' to '+ fileSavePath); 11 | 12 | // Start download 13 | var dl = downloader.download(fileUrl, fileSavePath) 14 | .start(); 15 | 16 | // Import generic examples for handling events and printing stats 17 | require('./_handleEvents')(dl); 18 | require('./_printStats')(dl); 19 | 20 | // Wait 10s before stopping and then 4s before resuming download 21 | setTimeout(function() { 22 | console.log('DEMO - Stopping download... will be resumed in 4s'); 23 | 24 | dl.stop(); 25 | 26 | setTimeout(function() { 27 | console.log('DEMO - Resuming download...'); 28 | dl.resume(); 29 | }, 4000); 30 | }, 10000); -------------------------------------------------------------------------------- /lib/Download.js: -------------------------------------------------------------------------------- 1 | var mtd = require('mt-downloader'); 2 | var fs = require('fs'); 3 | var util = require('util'); 4 | var EventEmitter = require('events').EventEmitter; 5 | 6 | var Download = function() { 7 | EventEmitter.call(this); 8 | 9 | this._reset(); 10 | 11 | this.url = ''; 12 | this.filePath = ''; 13 | this.options = {}; 14 | this.meta = {}; 15 | 16 | this._retryOptions = { 17 | _nbRetries: 0, 18 | maxRetries: 5, 19 | retryInterval: 5000 20 | }; 21 | }; 22 | 23 | util.inherits(Download, EventEmitter); 24 | 25 | Download.prototype._reset = function(first_argument) { 26 | this.status = 0; // -3 = destroyed, -2 = stopped, -1 = error, 0 = not started, 1 = started (downloading), 2 = error, retrying, 3 = finished 27 | this.error = ''; 28 | 29 | this.stats = { 30 | time: { 31 | start: 0, 32 | end: 0 33 | }, 34 | total: { 35 | size: 0, 36 | downloaded: 0, 37 | completed: 0 38 | }, 39 | past: { 40 | downloaded: 0 41 | }, 42 | present: { 43 | downloaded: 0, 44 | time: 0, 45 | speed: 0 46 | }, 47 | future: { 48 | remaining: 0, 49 | eta: 0 50 | }, 51 | threadStatus: { 52 | idle: 0, 53 | open: 0, 54 | closed: 0, 55 | failed: 0 56 | } 57 | }; 58 | }; 59 | 60 | Download.prototype.setUrl = function(url) { 61 | this.url = url; 62 | 63 | return this; 64 | }; 65 | 66 | Download.prototype.setFilePath = function(filePath) { 67 | this.filePath = filePath; 68 | 69 | return this; 70 | }; 71 | 72 | Download.prototype.setOptions = function(options) { 73 | if(!options || options == {}) { 74 | return this.options = {}; 75 | } 76 | 77 | // The "options" object will be directly passed to mt-downloader, so we need to conform to his format 78 | 79 | //To set the total number of download threads 80 | this.options.count = options.threadsCount || options.count || 2; 81 | 82 | //HTTP method 83 | this.options.method = options.method || 'GET'; 84 | 85 | //HTTP port 86 | this.options.port = options.port || 80; 87 | 88 | //If no data is received the download times out. It is measured in seconds. 89 | this.options.timeout = options.timeout/1000 || 5; 90 | 91 | //Control the part of file that needs to be downloaded. 92 | this.options.range = options.range || '0-100'; 93 | 94 | // Support customized header fields 95 | this.options.headers = options.headers || {}; 96 | 97 | return this; 98 | }; 99 | 100 | Download.prototype.setRetryOptions = function(options) { 101 | this._retryOptions.maxRetries = options.maxRetries || 5; 102 | this._retryOptions.retryInterval = options.retryInterval || 2000; 103 | 104 | return this; 105 | }; 106 | 107 | Download.prototype.setMeta = function(meta) { 108 | this.meta = meta; 109 | 110 | return this; 111 | }; 112 | 113 | Download.prototype.setStatus = function(status) { 114 | this.status = status; 115 | 116 | return this; 117 | }; 118 | 119 | Download.prototype.setError = function(error) { 120 | this.error = error; 121 | 122 | return this; 123 | }; 124 | 125 | Download.prototype._computeDownloaded = function() { 126 | if(!this.meta.threads) { return 0; } 127 | 128 | var downloaded = 0; 129 | this.meta.threads.forEach(function(thread) { 130 | downloaded += thread.position - thread.start; 131 | }); 132 | 133 | return downloaded; 134 | }; 135 | 136 | // Should be called on start, set the start timestamp (in seconds) 137 | Download.prototype._computeStartTime = function() { 138 | this.stats.time.start = Math.floor(Date.now() / 1000); 139 | }; 140 | 141 | // Should be called on end, set the end timestamp (in seconds) 142 | Download.prototype._computeEndTime = function() { 143 | this.stats.time.end = Math.floor(Date.now() / 1000); 144 | }; 145 | 146 | // Should be called on start, count size already downloaded (eg. resumed download) 147 | Download.prototype._computePastDownloaded = function() { 148 | this.stats.past.downloaded = this._computeDownloaded(); 149 | }; 150 | 151 | // Should be called on start compute total size 152 | Download.prototype._computeTotalSize = function() { 153 | var threads = this.meta.threads; 154 | 155 | if(!threads) { return 0; } 156 | 157 | this.stats.total.size = threads[threads.length-1].end - threads[0].start; 158 | }; 159 | 160 | Download.prototype._computeStats = function() { 161 | this._computeTotalSize(); 162 | this._computeTotalDownloaded(); 163 | this._computePresentDownloaded(); 164 | this._computeTotalCompleted(); 165 | this._computeFutureRemaining(); 166 | 167 | // Only compute those stats when downloading 168 | if(this.status == 1) { 169 | this._computePresentTime(); 170 | this._computePresentSpeed(); 171 | this._computeFutureEta(); 172 | this._computeThreadStatus(); 173 | } 174 | }; 175 | 176 | Download.prototype._computePresentTime = function() { 177 | this.stats.present.time = Math.floor(Date.now() / 1000) - this.stats.time.start; 178 | }; 179 | 180 | Download.prototype._computeTotalDownloaded = function() { 181 | this.stats.total.downloaded = this._computeDownloaded(); 182 | }; 183 | 184 | Download.prototype._computePresentDownloaded = function() { 185 | this.stats.present.downloaded = this.stats.total.downloaded - this.stats.past.downloaded; 186 | }; 187 | 188 | Download.prototype._computeTotalCompleted = function() { 189 | this.stats.total.completed = Math.floor((this.stats.total.downloaded) * 1000 / this.stats.total.size) / 10; 190 | }; 191 | 192 | Download.prototype._computeFutureRemaining = function() { 193 | this.stats.future.remaining = this.stats.total.size - this.stats.total.downloaded; 194 | }; 195 | 196 | Download.prototype._computePresentSpeed = function() { 197 | this.stats.present.speed = this.stats.present.downloaded / this.stats.present.time; 198 | }; 199 | 200 | Download.prototype._computeFutureEta = function() { 201 | this.stats.future.eta = this.stats.future.remaining / this.stats.present.speed; 202 | }; 203 | 204 | Download.prototype._computeThreadStatus = function() { 205 | var self = this; 206 | 207 | this.stats.threadStatus = { 208 | idle: 0, 209 | open: 0, 210 | closed: 0, 211 | failed: 0 212 | }; 213 | 214 | this.meta.threads.forEach(function(thread) { 215 | self.stats.threadStatus[thread.connection]++; 216 | }); 217 | }; 218 | 219 | Download.prototype.getStats = function() { 220 | if(!this.meta.threads) { 221 | return this.stats; 222 | } 223 | 224 | this._computeStats(); 225 | 226 | return this.stats; 227 | }; 228 | 229 | Download.prototype._destroyThreads = function() { 230 | if(this.meta.threads) { 231 | this.meta.threads.forEach(function(i){ 232 | if(i.destroy) { 233 | i.destroy(); 234 | } 235 | }); 236 | } 237 | }; 238 | 239 | Download.prototype.stop = function() { 240 | this.setStatus(-2); 241 | 242 | this._destroyThreads(); 243 | 244 | this.emit('stopped', this); 245 | }; 246 | 247 | Download.prototype.destroy = function() { 248 | var self = this; 249 | 250 | this._destroyThreads(); 251 | 252 | this.setStatus(-3); 253 | 254 | var filePath = this.filePath; 255 | var tmpFilePath = filePath; 256 | if (!filePath.match(/\.mtd$/)) { 257 | tmpFilePath += '.mtd'; 258 | } else { 259 | filePath = filePath.replace(new RegExp('(.mtd)*$', 'g'), ''); 260 | } 261 | 262 | fs.unlink(filePath, function() { 263 | fs.unlink(tmpFilePath, function() { 264 | self.emit('destroyed', this); 265 | }); 266 | }); 267 | }; 268 | 269 | Download.prototype.start = function() { 270 | var self = this; 271 | 272 | self._reset(); 273 | self._retryOptions._nbRetries = 0; 274 | 275 | this.options.onStart = function(meta) { 276 | self.setStatus(1); 277 | self.setMeta(meta); 278 | 279 | self.setUrl(meta.url); 280 | 281 | self._computeStartTime(); 282 | self._computePastDownloaded(); 283 | self._computeTotalSize(); 284 | 285 | self.emit('start', self); 286 | }; 287 | 288 | this.options.onEnd = function(err, result) { 289 | // If stopped or destroyed, do nothing 290 | if(self.status == -2 || self.status == -3) { 291 | return; 292 | } 293 | 294 | // If we encountered an error and it's not an "Invalid file path" error, we try to resume download "maxRetries" times 295 | if(err && (''+err).indexOf('Invalid file path') == -1 && self._retryOptions._nbRetries < self._retryOptions.maxRetries) { 296 | self.setStatus(2); 297 | self._retryOptions._nbRetries++; 298 | 299 | setTimeout(function() { 300 | self.resume(); 301 | 302 | self.emit('retry', self); 303 | }, self._retryOptions.retryInterval); 304 | // "Invalid file path" or maxRetries reached, emit error 305 | } else if(err) { 306 | self._computeEndTime(); 307 | 308 | self.setError(err); 309 | self.setStatus(-1); 310 | 311 | self.emit('error', self); 312 | // No error, download ended successfully 313 | } else { 314 | self._computeEndTime(); 315 | 316 | self.setStatus(3); 317 | 318 | self.emit('end', self); 319 | } 320 | }; 321 | 322 | this._downloader = new mtd(this.filePath, this.url, this.options); 323 | 324 | this._downloader.start(); 325 | 326 | return this; 327 | }; 328 | 329 | Download.prototype.resume = function() { 330 | this._reset(); 331 | 332 | var filePath = this.filePath; 333 | if (!filePath.match(/\.mtd$/)) { 334 | filePath += '.mtd'; 335 | } 336 | 337 | this._downloader = new mtd(filePath, null, this.options); 338 | 339 | this._downloader.start(); 340 | 341 | return this; 342 | }; 343 | 344 | // For backward compatibility, will be removed in next releases 345 | Download.prototype.restart = util.deprecate(function() { 346 | return this.resume(); 347 | }, 'Download `restart()` is deprecated, please use `resume()` instead.'); 348 | 349 | module.exports = Download; -------------------------------------------------------------------------------- /lib/Downloader.js: -------------------------------------------------------------------------------- 1 | var Download = require('./Download'); 2 | var Formatters = require('./Formatters'); 3 | var util = require('util'); 4 | 5 | var extend = function(target) { 6 | var sources = [].slice.call(arguments, 1); 7 | sources.forEach(function (source) { 8 | for (var prop in source) { 9 | target[prop] = source[prop]; 10 | } 11 | }); 12 | return target; 13 | }; 14 | 15 | /* 16 | ------------------------------------------ 17 | - Downloader class 18 | ------------------------------------------ 19 | */ 20 | var Downloader = function() { 21 | this._downloads = []; 22 | }; 23 | 24 | Downloader.prototype._defaultOptions = { 25 | //To set the total number of download threads 26 | threadsCount: 2, //(Default: 6) 27 | 28 | //HTTP method 29 | method: 'GET', //(Default: GET) 30 | 31 | //HTTP port 32 | port: 80, //(Default: 80) 33 | 34 | //If no data is received the download times out. It is measured in seconds. 35 | timeout: 5000, //(Default: 5 seconds) 36 | 37 | //Control the part of file that needs to be downloaded. 38 | range: '0-100', //(Default: '0-100') 39 | }; 40 | 41 | Downloader.prototype.download = function(url, filePath, options) { 42 | var options = extend({}, this._defaultOptions, options); 43 | 44 | var dl = new Download(); 45 | 46 | dl.setUrl(url); 47 | dl.setFilePath(filePath); 48 | dl.setOptions(options); 49 | 50 | this._downloads.push(dl); 51 | 52 | return dl; 53 | }; 54 | 55 | Downloader.prototype.resumeDownload = function(filePath) { 56 | var dl = new Download(); 57 | 58 | if (!filePath.match(/\.mtd$/)) { 59 | filePath += '.mtd'; 60 | } 61 | 62 | dl.setUrl(null); 63 | dl.setFilePath(filePath); 64 | dl.setOptions({}); 65 | 66 | this._downloads.push(dl); 67 | 68 | return dl; 69 | }; 70 | 71 | // For backward compatibility, will be removed in next releases 72 | Downloader.prototype.restart = util.deprecate(function(filePath) { 73 | return this.resumeDownload(filePath); 74 | }, 'Downloader `restart(filePath)` is deprecated, please use `resumeDownload(filePath)` instead.'); 75 | 76 | Downloader.prototype.getDownloadByUrl = function(url) { 77 | var dlFound = null; 78 | 79 | this._downloads.forEach(function(dl) { 80 | if(dl.url === url || (dl.meta && dl.meta.url && dl.meta.url == url)) { 81 | dlFound = dl; 82 | } 83 | }); 84 | 85 | return dlFound; 86 | }; 87 | 88 | Downloader.prototype.getDownloadByFilePath = function(filePath) { 89 | var dlFound = null; 90 | 91 | var mtdRegex = new RegExp('(.mtd)*$', 'g'); 92 | 93 | filePath = filePath.replace(mtdRegex, ''); 94 | 95 | this._downloads.forEach(function(dl) { 96 | var dlFilePath = dl.filePath.replace(mtdRegex, ''); 97 | 98 | if(dlFilePath === filePath) { 99 | dlFound = dl; 100 | } 101 | }); 102 | 103 | return dlFound; 104 | }; 105 | 106 | Downloader.prototype.removeDownloadByFilePath = function(filePath) { 107 | var dlFound = false; 108 | 109 | var mtdRegex = new RegExp('(.mtd)*$', 'g'); 110 | 111 | filePath = filePath.replace(mtdRegex, ''); 112 | 113 | for(var i = 0; i < this._downloads.length; i++) { 114 | var dlFilePath = this._downloads[i].filePath.replace(mtdRegex, ''); 115 | 116 | if(dlFilePath === filePath) { 117 | this._downloads.splice(i, 1); 118 | dlFound = true; 119 | } 120 | } 121 | 122 | return dlFound; 123 | }; 124 | 125 | Downloader.prototype.getDownloads = function() { 126 | return this._downloads; 127 | }; 128 | 129 | Downloader.prototype.Formatters = Formatters; 130 | Downloader.Formatters = Formatters; 131 | 132 | module.exports = Downloader; -------------------------------------------------------------------------------- /lib/Formatters.js: -------------------------------------------------------------------------------- 1 | var moment = require('moment'); 2 | 3 | var _floor = function(val) { 4 | return Math.floor(val); 5 | }; 6 | 7 | var Formatters = { 8 | speed: function(speed) { 9 | var str; 10 | speed *= 8; 11 | if (speed > 1024 * 1024) str = _floor(speed * 10 / (1024 * 1024)) / 10 + ' Mbps'; 12 | else if (speed > 1024) str = _floor(speed * 10 / 1024) / 10 + ' Kbps'; 13 | else str = _floor(speed) + ' bps'; 14 | return str + ''; 15 | }, 16 | 17 | elapsedTime: function(seconds) { 18 | return _floor(seconds) + 's'; 19 | }, 20 | 21 | remainingTime: function(seconds) { 22 | return moment.duration(seconds, 'seconds').humanize(); 23 | } 24 | }; 25 | 26 | module.exports = Formatters; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mt-files-downloader", 3 | "version": "0.2.5", 4 | "description": "Download manager with multiple features : download stats, stop & resume, auto-retry (continue) in case of error, events, etc.", 5 | "main": "lib/Downloader.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/leeroybrun/node-mt-files-downloader.git" 12 | }, 13 | "author": "Leeroy Brun ", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/leeroybrun/node-mt-files-downloader/issues" 17 | }, 18 | "homepage": "https://github.com/leeroybrun/node-mt-files-downloader", 19 | "dependencies": { 20 | "moment": "^2.9.0", 21 | "mt-downloader": "^0.2.10" 22 | } 23 | } 24 | --------------------------------------------------------------------------------