Skip to content

Commit

Permalink
Added download from previous file #13, PR #34, #54
Browse files Browse the repository at this point in the history
  • Loading branch information
hgouveia committed Mar 14, 2022
1 parent c2c5714 commit d622e9c
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 6 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ these are the default values
method: 'GET', // Request Method Verb
headers: {}, // Custom HTTP Header ex: Authorization, User-Agent
timeout: -1, // Request timeout in milliseconds (-1 use default), is the equivalent of 'httpRequestOptions: { timeout: value }' (also applied to https)
resumeIfFileExists: false, // it will resume if a file already exists and is not completed, you might want to set removeOnStop and removeOnFail to false. If you used pipe for compression it will produce corrupted files
fileName: string|cb(fileName, filePath, contentType)|{name, ext}, // Custom filename when saved
retry: false, // { maxRetries: number, delay: number in ms } or false to disable (default)
forceResume: false, // If the server does not return the "accept-ranges" header, can be force if it does support it
Expand Down Expand Up @@ -120,7 +121,26 @@ for `httpsRequestOptions` the available options are detailed in here https://nod
| getTotalSize | gets the total file size from the server |
| getDownloadPath | gets the full path where the file will be downloaded (available after the start phase) |
| isResumable | return true/false if the download can be resumable (available after the start phase) |
| getResumeState | Get the state required to resume the download after restart. This state can be passed back to `resumeFromFile()` to resume a download |
| resumeFromFile | `resumeFromFile(filePath?: string, state?: IResumeState)` Resume the download from a previous file path, if the state is not provided it will try to fetch from the information the headers and filePath, @see `resumeIfFileExists` option |

usage of `resumeFromFile`

```javascript
const downloadDir = 'D:/TEMP';
const { DownloaderHelper } = require('node-downloader-helper');
const dl = new DownloaderHelper('https://proof.ovh.net/files/1Gb.dat', downloadDir);
dl.on('end', () => console.log('Download Completed'));
dl.on('error', (err) => console.log('Download Failed', err));

// option 1
const prevFilePath = `${downloadDir}/1Gb.dat`;
dl.resumeFromFile(prevFilePath).catch(err => console.error(err));

// option 2
const prevState = dl.getResumeState(); // this should be stored in a file, localStorage, db, etc in a previous process for example on 'stop'
dl.resumeFromFile(prevState.filePath, prevState).catch(err => console.error(err));
```

## Events

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "node-downloader-helper",
"version": "2.0.1",
"version": "2.1.0",
"description": "A simple http file downloader for node.js",
"main": "./dist/index.js",
"types": "./types/index.d.ts",
Expand Down
81 changes: 76 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ export class DownloaderHelper extends EventEmitter {
removeOnFail: true,
progressThrottle: 1000,
httpRequestOptions: {},
httpsRequestOptions: {}
httpsRequestOptions: {},
resumeIfFileExists: false,
};
this.__opts = Object.assign({}, this.__defaultOpts);
this.__pipes = [];
Expand Down Expand Up @@ -83,10 +84,29 @@ export class DownloaderHelper extends EventEmitter {
* @memberof DownloaderHelper
*/
start() {
return new Promise((resolve, reject) => {
const startPromise = () => new Promise((resolve, reject) => {
this.__promise = { resolve, reject };
this.__start();
});

// this will determine the file path from the headers
// and attempt to get the file size and resume if possible
if (this.__opts.resumeIfFileExists && this.state !== this.__states.RESUMED) {
return this.getTotalSize().then(({ name, total }) => {
const override = this.__opts.override;
this.__opts.override = true;
this.__filePath = this.__getFilePath(name);
this.__opts.override = override;
if (this.__filePath && fs.existsSync(this.__filePath)) {
const fileSize = this.__getFilesizeInBytes(this.__filePath);
return fileSize !== total
? this.resumeFromFile(this.__filePath, { total, fileName: name })
: startPromise();
}
return startPromise();
});
}
return startPromise();
}

/**
Expand Down Expand Up @@ -132,7 +152,7 @@ export class DownloaderHelper extends EventEmitter {
this.__setState(this.__states.RESUMED);
if (this.__isResumable) {
this.__isResumed = true;
this.__options['headers']['range'] = 'bytes=' + this.__downloaded + '-';
this.__options['headers']['range'] = `bytes=${this.__downloaded}-`;
}
this.emit('resume', this.__isResumed);
return this.__start();
Expand Down Expand Up @@ -287,14 +307,18 @@ export class DownloaderHelper extends EventEmitter {
* @memberof DownloaderHelper
*/
getTotalSize() {
const options = this.__getOptions('HEAD', this.url, this.__headers);
const headers = Object.assign({}, this.__headers);
if (headers.hasOwnProperty('range')) {
delete headers['range'];
}
const options = this.__getOptions('HEAD', this.url, headers);
return new Promise((resolve, reject) => {
const request = this.__protocol.request(options, response => {
if (this.__isRequireRedirect(response)) {
const redirectedURL = /^https?:\/\//.test(response.headers.location)
? response.headers.location
: new URL(response.headers.location, this.url).href;
const options = this.__getOptions('HEAD', redirectedURL, this.__headers);
const options = this.__getOptions('HEAD', redirectedURL, headers);
const request = this.__protocol.request(options, response => {
if (response.statusCode !== 200) {
reject(new Error(`Response status was ${response.statusCode}`));
Expand All @@ -319,6 +343,53 @@ export class DownloaderHelper extends EventEmitter {
});
}

/**
* Get the state required to resume the download after restart. This state
* can be passed back to `resumeFromFile()` to resume a download
*
* @returns {Object} Returns the state required to resume
* @memberof DownloaderHelper
*/
getResumeState() {
return {
downloaded: this.__downloaded,
filePath: this.__filePath,
fileName: this.__fileName,
total: this.__total,
};
}

/**
* Resume the download from a previous file path
*
* @param {string} filePath - The path to the file to resume from ex: C:\Users\{user}\Downloads\file.txt
* @param {Object} state - (optionl) resume download state, if not provided it will try to fetch from the headers and filePath
*
* @returns {Promise<boolean>} - Returns the same result as `start()`
* @memberof DownloaderHelper
*/
resumeFromFile(filePath, state = {}) {
this.__opts.override = true;
this.__filePath = filePath;
return ((state.total && state.fileName)
? Promise.resolve({ name: state.fileName, total: state.total })
: this.getTotalSize())
.then(({ name, total }) => {
this.__total = state.total || total;
this.__fileName = state.fileName || name;
this.__downloaded = state.downloaded || this.__getFilesizeInBytes(this.__filePath);
this.__options['headers']['range'] = `bytes=${this.__downloaded}-`;
this.__isResumed = true;
this.__isResumable = true;
this.__setState(this.__states.RESUMED);
this.emit('resume', this.__isResumed);
return new Promise((resolve, reject) => {
this.__promise = { resolve, reject };
this.__start();
});
});
}

__start() {
if (!this.__isRedirected &&
this.state !== this.__states.RESUMED) {
Expand Down
30 changes: 30 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ interface FileRenamedStats {
prevFileName: string;
}

interface IResumeState {
downloaded?: number;
filePath?: string;
fileName?: string;
total?: number;
}

interface ErrorStats {
/** Error message */
message: string;
Expand Down Expand Up @@ -124,6 +131,10 @@ interface DownloaderHelperOptions {
/** Custom filename when saved */
fileName?: string | FilenameCallback | FilenameDefinition;
retry?: boolean | RetryOptions;
/* Request timeout in milliseconds (-1 use default), is the equivalent of 'httpRequestOptions: { timeout: value }' (also applied to https) */
timeout?: number;
/** it will resume if a file already exists and is not completed, you might want to set removeOnStop and removeOnFail to false. If you used pipe for compression it will produce corrupted files */
resumeIfFileExists?: boolean;
/** If the server does not return the "accept-ranges" header, can be force if it does support it */
forceResume?: boolean;
/** remove the file when is stopped (default:true) */
Expand Down Expand Up @@ -247,4 +258,23 @@ export class DownloaderHelper extends EventEmitter {
* @memberof EventEmitter
*/
on<E extends keyof DownloadEvents>(event: E, callback: DownloadEvents[E]): any;

/**
* Get the state required to resume the download after restart. This state
* can be passed back to `resumeFromFile()` to resume a download
*
* @returns {IResumeState} Returns the state required to resume
* @memberof DownloaderHelper
*/
getResumeState(): IResumeState;

/**
*
* @param {string} filePath - The path to the file to resume from ex: C:\Users\{user}\Downloads\file.txt
* @param {IResumeState} state - (optionl) resume download state, if not provided it will try to fetch from the headers and filePath
*
* @returns {Promise<boolean>} - Returns the same result as `start()`
* @memberof DownloaderHelper
*/
resumeFromFile(filePath: string, state?: IResumeState): Promise<boolean>;
}

0 comments on commit d622e9c

Please sign in to comment.