const fs = require('fs');
const path = require('path');
const {EOL, tmpdir} = require('os');
const {lookup: mime} = require('mime-types');
/**
* @external got
* @desc A human-friendly and powerful HTTP request library
* @see {@link https://www.npmjs.com/package/got|got}
*/
const got = require('got');
/**
* @external ProgressPromise
* @desc Promise subclass with mechanism to report progress before resolving
* @see {@link https://www.npmjs.com/package/progress-promise|progress-promise}
*/
const ProgressPromise = require('progress-promise');
/**
* The root domain
*
* @constant
* @type {string}
* @default <a href="https://transfer.sh">https://transfer.sh</a>
* @todo Make this configurable
*/
const DOMAIN = 'https://transfer.sh';
/**
* Class representing a Transfer Error
*
* @extends {Error}
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error">Error</a>
*/
class TransferError extends Error {
get name() { return 'TransferError' }
}
/**
* Class representing a Transfer
*
* @version 0.4.0
*/
class Transfer {
/**
* @typedef {Object} TransferOptions
* @property {string} [filename] - A custom filename for the upload
*/
/**
* @param {string} fileInput - File path
* @param {Object} [options={}] - {@link external:got|got} options
*/
constructor(fileInput, options={}) {
/**
* The input file path/URL
*
* @type {string}
*/
this.fileInput = fileInput;
/**
* The size of the file in bytes
*
* @type {number}
*/
this.fileSize = 0;
/** A {@link external:got|got} options object
*
* @type {Object}
* @see <a href="https://github.com/sindresorhus/got#options">got options</a>
*/
this.options = options;
}
/**
* Adds <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type">Content-Type</a>
* header to the request
*
* @protected
* @since 0.4.0
* @param {Object} options - An HTTP options object
* @param {string} [file=null] - The input file
*/
_contentType(options, file=null) {
if(!options.headers['Content-Type']) {
// istanbul ignore else
if(file !== null) {
options.headers['Content-Type'] =
mime(path.extname(file)) || 'text/plain';
} else {
options.headers['Content-Type'] =
'application/octet-stream';
}
}
}
/**
* Upload a file to {@link DOMAIN}
*
* @param {string} [filename] - The name of the uploaded file
* @returns {external:ProgressPromise.<string|TransferError>} -
* The link if resolved, a TransferError if rejected
*/
upload(filename) {
const self = this;
if(!filename) filename = path.basename(self.fileInput);
const fileURL = `${DOMAIN}/${filename}`;
return new ProgressPromise((resolve, reject, progress) => {
if(!filename) return reject(new TransferError('Missing file input'));
try {
self.fileSize = fs.statSync(self.fileInput).size;
fs.accessSync(self.fileInput, fs.R_OK);
} catch(error) {
switch(error.code) {
case 'ENOENT':
return reject(new TransferError(
`File not found: '${self.fileInput}'`
));
case 'EACCES':
return reject(new TransferError(
`Cannot read file: '${self.fileInput}'`
));
// istanbul skip next
default:
return reject(error);
}
}
self._contentType(self.options, self.fileInput);
self.options.body = fs.createReadStream(self.fileInput);
got.put(fileURL, self.options)
.on('uploadProgress', p => {
// The uploaded size is roughly 1.016 times larger
// than the actual size, likely due to the metadata
const curr = parseInt(p.transferred / 1.016 + 0.5);
progress({
current: (curr < self.fileSize) ? curr : self.fileSize,
total: self.fileSize,
task: 'Upload'
});
}).then((res) => resolve(res.body)).catch(reject);
});
}
/**
* Download a file from {@link DOMAIN}
*
* @param {string} destination - Destination path
* @returns {external:ProgressPromise.<string|TransferError>} -
* The path if resolved, a {@link TransferError} if rejected
* @todo Support decrypting
*/
download(destination) {
const self = this;
const url = self.fileInput;
const filePath = path.resolve(destination || path.basename(url));
return new ProgressPromise((resolve, reject, progress) => {
if(!url) return reject(new TransferError('Missing file URL'));
self.options.method = 'GET';
got.stream(url, self.options)
.on('downloadProgress', p => {
self.fileSize = p.total;
progress({
current: p.transferred,
total: p.total,
task: 'Download'
});
})
.pipe(fs.createWriteStream(filePath))
.on('finish', () => resolve(filePath))
.on('error', reject);
});
}
}
module.exports = {Transfer, TransferError};