zippycli
Version:
An unofficial Zippyshare CLI
388 lines (296 loc) • 9.88 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.Download = void 0;
var _path = require("path");
var _command = require("@oclif/command");
var _fsExtra = _interopRequireDefault(require("fs-extra"));
var _zsExtract = require("zs-extract");
var _constants = require("../constants");
var _util = require("../util");
var _progress = require("../progress");
var _command2 = require("../command");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
/**
* Download command.
*/
class Download extends _command2.Command {
/**
* Aliases.
*/
/**
* Description.
*/
/**
* Examples.
*/
/**
* Flags.
*/
/**
* Arguments.
*/
/**
* Handler.
*/
async run() {
const {
args,
flags
} = this.parse(Download);
const source = args.source;
const {
timeout,
update
} = flags;
const mtime = flags.mtime || false;
const outdir = flags.dir || '';
const file = flags.output || '';
const input = flags.input || false;
if (file && input) {
throw new Error('Both file and input arguments found');
}
const sources = flags.input ? await this._readInputFile(source) : [source];
const req = this._initRequest(timeout * 1000);
let errors = false;
for (let i = 0; i < sources.length; i++) {
const source = sources[i];
if (i) {
this.log('');
}
try {
// eslint-disable-next-line no-await-in-loop
await this._handleSource(source, outdir, file, mtime, update, req);
} catch (err) {
errors = true;
this.log(`error: ${err.toString()}`);
}
}
if (errors) {
this.exit(1);
}
}
/**
* Handle an individual source.
*
* @param source The source.
* @param outdir Output dir.
* @param outfile Output files.
* @param mtime Modification time.
* @param updateInterval Update interval.
* @param req Request factory.
*/
async _handleSource(source, outdir, outfile, mtime, updateInterval, req) {
this.log(`source: ${source}`);
const info = await (0, _zsExtract.extract)(source, req);
const {
download
} = info;
const filename = outfile || info.filename;
if (!filename) {
throw new Error('No filename extracted or specified');
}
this.log(`download: ${download}`);
this.log(`filename: ${filename}`);
if (outdir) {
this.log(`dir: ${outdir}`);
}
const filepath = outdir ? (0, _path.join)(outdir, filename) : filename; // Get file stat and size if a file.
const stat = await (0, _util.fstat)(filepath); // If path already exists, and is not file, then error.
if (stat && !stat.isFile()) {
throw new Error('Output filename already exists');
} // Make a HEAD request to the download URL, to get file headers.
const headResponse = await new Promise((resolve, reject) => {
req({
url: download,
method: 'HEAD'
}, (err, response) => {
if (err) {
reject(err);
return;
}
resolve(response);
});
}); // Verify the status code.
const {
statusCode
} = headResponse;
if (statusCode !== 200) {
throw new Error(`Invalid status code: ${statusCode}`);
} // Read headers.
const contentLength = headResponse.headers['content-length'];
const acceptRanges = headResponse.headers['accept-ranges'];
const lastModified = headResponse.headers['last-modified'];
this.log(`content-length: ${contentLength}`);
this.log(`accept-ranges: ${acceptRanges}`);
this.log(`last-modified: ${lastModified}`); // Parse the content-length header.
const contentLengthI = parseInt(contentLength || '', 10); // If path exists and is file, verify size.
if (stat) {
if (contentLengthI || contentLengthI === 0) {
if (contentLengthI === stat.size) {
this.log('done: Already retrieved');
return;
}
throw new Error(`Invalid existing file size: ${stat.size}`);
}
throw new Error('Could not verify correct file size');
} // Determine if possible to resume any partial downloads.
const canResume = acceptRanges === 'bytes'; // Parse the date modified.
const dateModified = (0, _util.parseDate)(lastModified || ''); // Create part file name.
const partFilename = this._partialFilename(filename);
const partFilepath = outdir ? (0, _path.join)(outdir, partFilename) : partFilename;
this.log(`filename-partial: ${partFilename}`); // Stat the part file if exists, throw if not file.
const partFileStat = await (0, _util.fstat)(partFilepath);
if (partFileStat && !partFileStat.isFile()) {
throw new Error('Partial file path exists but not a file');
} // Get offset to resume download from if possible.
const resumeFrom = canResume && partFileStat ? partFileStat.size : 0;
if (resumeFrom) {
this.log(`resume-from: ${resumeFrom}`);
}
if (resumeFrom > contentLengthI) {
throw new Error(`Partial larger than expected: ${resumeFrom}`);
} // Open the file for append if resuming, else write.
await _fsExtra.default.ensureFile(partFilepath);
const file = _fsExtra.default.createWriteStream(partFilepath, {
flags: resumeFrom ? 'a' : 'w'
}); // Only download if partial file is not complete.
if (contentLengthI && resumeFrom !== contentLengthI) {
this.log(`download-start: ${(0, _util.dateHumanTimestamp)()}`); // Start a progress monitor.
const progress = new _progress.Progress(contentLengthI, resumeFrom);
progress.start(updateInterval, this._transferProgressOutputInit()); // Start download, monitoring progress.
const dl = this._download(file, download, req, resumeFrom);
dl.stream.on('data', data => {
progress.add(data.length);
}); // Await completion.
try {
await dl.complete;
} finally {
progress.end();
this._transferProgressOutputAfter();
}
this.log(`download-end: ${(0, _util.dateHumanTimestamp)()}`);
} // Otherwise just close file.
else {
this.log('already-complete: partial file already complete');
await new Promise(resolve => {
file.end(resolve);
});
}
this.log(`verify-size: ${contentLengthI}`); // Verify partial file size after download.
const partFileDoneStat = await (0, _util.fstat)(partFilepath);
if (!partFileDoneStat || !partFileDoneStat.isFile()) {
throw new Error('Failed to verify download file size');
}
if (partFileDoneStat.size !== contentLengthI) {
const {
size
} = partFileDoneStat;
throw new Error(`Unexpected download size: ${size}`);
} // Set mtime if requested and available.
if (dateModified && mtime) {
const time = dateModified.getTime() / 1000;
this.log(`setting-mtime: ${time}`);
await _fsExtra.default.utimes(partFilepath, time, time);
} // Move partial.
this.log(`saving-partial: ${partFilename}`);
await _fsExtra.default.move(partFilepath, filepath); // All done.
this.log(`done: ${filename}`);
}
/**
* Download file to a file stream.
*
* @param file File stream.
* @param url Download URL.
* @param req Request factory.
* @param resume Resume offset.
* @returns Stream object and a complete promise.
*/
_download(file, url, req, resume) {
const headers = {};
if (resume) {
headers.Range = `bytes=${resume}-`;
}
const statusCodeExpected = resume ? 206 : 200;
let error = null;
const stream = req({
url,
headers
});
stream.once('response', response => {
const {
statusCode
} = response;
if (statusCode === statusCodeExpected) {
return;
}
stream.abort();
error = new Error(`Invalid status code: ${statusCode}`);
});
const complete = (0, _util.pipelineP)(stream, file).then(() => {
if (error) {
throw error;
}
}).catch(err => {
throw error || err;
});
return {
stream,
complete
};
}
/**
* Get a partial name from filename.
*
* @param filename The filename.
* @returns Partial name.
*/
_partialFilename(filename) {
return `${_constants.PARTIAL_FILE_PREFIX}${filename}`;
}
}
exports.Download = Download;
_defineProperty(Download, "aliases", ['dl', 'd']);
_defineProperty(Download, "description", 'download file from source');
_defineProperty(Download, "examples", []);
_defineProperty(Download, "flags", {
help: _command.flags.help({
char: 'h'
}),
output: _command.flags.string({
char: 'o',
description: 'output file'
}),
dir: _command.flags.string({
char: 'd',
description: 'output directory'
}),
input: _command.flags.boolean({
char: 'i',
description: 'source is input file with a URL on each line'
}),
mtime: _command.flags.boolean({
char: 'm',
description: 'use server modified time if available'
}),
timeout: _command.flags.integer({
char: 't',
description: 'request timeout in seconds',
default: _constants.DEFAULT_TIMEOUT
}),
update: _command.flags.integer({
char: 'u',
description: 'update interval in milliseconds',
default: _constants.DEFAULT_UPDATE_INTERVAL
})
});
_defineProperty(Download, "args", [{
name: 'source',
required: true,
description: 'source to download from'
}]);
var _default = Download;
exports.default = _default;
//# sourceMappingURL=download.js.map