@uploadx/core
Version:
Node.js resumable upload middleware
162 lines (161 loc) • 5.96 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Tus = exports.TUS_VERSION = exports.TUS_RESUMABLE = void 0;
exports.serializeMetadata = serializeMetadata;
exports.parseMetadata = parseMetadata;
exports.tus = tus;
const storages_1 = require("../storages");
const utils_1 = require("../utils");
const base_handler_1 = require("./base-handler");
exports.TUS_RESUMABLE = '1.0.0';
exports.TUS_VERSION = '1.0.0';
function serializeMetadata(obj) {
return Object.entries(obj)
.map(([key, value]) => `${key} ${Buffer.from(String(value)).toString('base64')}`)
.toString();
}
function parseMetadata(encoded = '') {
const kvPairs = encoded.split(',').map(kv => kv.split(' '));
const metadata = Object.create(storages_1.Metadata.prototype);
for (const [key, value] of kvPairs) {
if (key)
metadata[key] = value ? Buffer.from(value, 'base64').toString() : '';
}
return metadata;
}
/**
* [tus resumable upload protocol](https://github.com/tus/tus-resumable-upload-protocol/blob/master/protocol.md)
*/
class Tus extends base_handler_1.BaseHandler {
get extension() {
const _extensions = ['creation', 'creation-with-upload', 'termination', 'checksum'];
if (this.storage.config.expiration)
_extensions.push('expiration');
return _extensions;
}
/**
* Sends current server configuration
*/
async options(req, res) {
const headers = {
'Tus-Extension': this.extension.toString(),
'Tus-Max-Size': this.storage.maxUploadSize,
'Tus-Checksum-Algorithm': this.storage.checksumTypes.toString()
};
this.send(res, { statusCode: 204, headers });
return {};
}
/**
* Create a file and send url to client
*/
async post(req, res) {
const metadataHeader = (0, utils_1.getHeader)(req, 'upload-metadata', true);
const metadata = parseMetadata(metadataHeader);
const config = { metadata };
config.userId = this.getUserId(req, res);
config.size = (0, utils_1.getHeader)(req, 'upload-length');
let file = await this.storage.create(req, config);
// 'creation-with-upload' block
if ((0, utils_1.typeis)(req, ['application/offset+octet-stream'])) {
const contentLength = +(0, utils_1.getHeader)(req, 'content-length');
file = await this.storage.write({ ...file, start: 0, body: req, contentLength });
}
const headers = this.buildHeaders(file, { Location: this.buildFileUrl(req, file) });
file.bytesWritten > 0 && (headers['Upload-Offset'] = file.bytesWritten);
(0, utils_1.setHeaders)(res, headers);
if (file.status === 'completed')
return file;
const statusCode = file.bytesWritten > 0 ? 200 : 201;
this.send(res, { statusCode });
return file;
}
/**
* Write a chunk to file
*/
async patch(req, res) {
const id = await this.getAndVerifyId(req, res);
const metadataHeader = (0, utils_1.getHeader)(req, 'upload-metadata', true);
const metadata = metadataHeader && parseMetadata(metadataHeader);
metadata && (await this.storage.update({ id }, { metadata, id }));
const start = Number((0, utils_1.getHeader)(req, 'upload-offset'));
const contentLength = +(0, utils_1.getHeader)(req, 'content-length');
const { checksumAlgorithm, checksum } = this.extractChecksum(req);
const file = await this.storage.write({
start,
id,
body: req,
contentLength,
checksumAlgorithm,
checksum
});
const headers = this.buildHeaders(file, { 'Upload-Offset': file.bytesWritten });
(0, utils_1.setHeaders)(res, headers);
if (file.status === 'completed')
return file;
this.send(res, { statusCode: 204 });
return file;
}
/**
* Return chunk offset
*/
async head(req, res) {
const id = await this.getAndVerifyId(req, res);
const file = await this.storage.write({ id });
const headers = this.buildHeaders(file, {
'Upload-Offset': file.bytesWritten,
'Upload-Length': file.size,
'Upload-Metadata': serializeMetadata(file.metadata)
});
this.send(res, { statusCode: 200, headers });
return {};
}
/**
* Delete upload
*/
async delete(req, res) {
const id = await this.getAndVerifyId(req, res);
const [file] = await this.storage.delete({ id });
this.send(res, { statusCode: 204 });
return file;
}
buildHeaders(file, headers = {}) {
if (file.expiredAt)
headers['Upload-Expires'] = new Date(file.expiredAt).toUTCString();
return headers;
}
send(res, { statusCode, headers = {}, body }) {
headers['Tus-Resumable'] = exports.TUS_RESUMABLE;
super.send(res, { statusCode, headers, body });
}
extractChecksum(req) {
const [checksumAlgorithm, checksum] = (0, utils_1.getHeader)(req, 'upload-checksum')
.split(/\s+/)
.filter(Boolean);
return { checksumAlgorithm, checksum };
}
}
exports.Tus = Tus;
/**
* Basic express wrapper
* @example
* ```js
* app.use('/files', tus({directory: '/tmp', maxUploadSize: '250GB'}));
* ```
*/
function tus(options = {}) {
return new Tus(options).handle;
}
/**
* Express wrapper
*
* - express ***should*** respond to the client when the upload complete and handle errors and GET requests
* @example
* ```js
* app.all('/files', tus.upload({ storage }), (req, res, next) => {
* if (req.method === 'GET') return res.sendStatus(404);
* console.log('File upload complete: ', req.body.name);
* return res.sendStatus(204);
* });
* ```
*/
tus.upload = (options = {}) => new Tus(options).upload;