@uploadx/core
Version:
Node.js resumable upload middleware
151 lines (150 loc) • 5.93 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Uploadx = void 0;
exports.rangeParser = rangeParser;
exports.uploadx = uploadx;
const url_1 = __importDefault(require("url"));
const utils_1 = require("../utils");
const base_handler_1 = require("./base-handler");
function rangeParser(rangeHeader = '') {
const parts = rangeHeader.split(/\s+|\//);
const size = parseInt(parts[2]);
const start = parseInt(parts[1]);
return { start, size };
}
/**
* [X-headers protocol implementation](https://github.com/kukhariev/node-uploadx/blob/master/proto.md#requests-overview)
*/
class Uploadx extends base_handler_1.BaseHandler {
/**
* Create File from request and send a file url to client
*/
async post(req, res) {
const metadata = await this.getMetadata(req);
const config = { metadata };
config.userId = this.getUserId(req, res);
config.size = (0, utils_1.getHeader)(req, 'x-upload-content-length');
config.contentType = (0, utils_1.getHeader)(req, 'x-upload-content-type');
const file = await this.storage.create(req, config);
const headers = this.buildHeaders(file, { Location: this.buildFileUrl(req, file) });
file.bytesWritten > 0 && (headers['Range'] = `bytes=0-${file.bytesWritten - 1}`);
(0, utils_1.setHeaders)(res, headers);
if (file.status === 'completed')
return file;
const response = await this.storage.onCreate(file);
response.statusCode = file.bytesWritten > 0 ? 200 : 201;
this.send(res, response);
return file;
}
async patch(req, res) {
const id = await this.getAndVerifyId(req, res);
const metadata = await this.getMetadata(req);
const file = await this.storage.update({ id }, metadata);
const headers = this.buildHeaders(file, { Location: this.buildFileUrl(req, file) });
(0, utils_1.setHeaders)(res, headers);
if (file.status === 'completed')
return file;
const response = await this.storage.onUpdate(file);
this.send(res, response);
return file;
}
/**
* Write a chunk to file or/and return chunk offset
*/
async put(req, res) {
const id = await this.getAndVerifyId(req, res);
const contentRange = (0, utils_1.getHeader)(req, 'content-range');
const contentLength = +(0, utils_1.getHeader)(req, 'content-length');
const { start, size = NaN } = contentRange ? rangeParser(contentRange) : { start: 0 };
const { checksumAlgorithm, checksum } = this.extractChecksum(req);
const file = await this.storage.write({
start,
id,
body: req,
size,
contentLength,
checksumAlgorithm,
checksum
});
const headers = this.buildHeaders(file);
if (file.status === 'completed')
return file;
headers['Range'] = `bytes=0-${file.bytesWritten - 1}`;
res.statusMessage = 'Resume Incomplete';
this.send(res, { statusCode: Uploadx.RESUME_STATUS_CODE, headers });
return file;
}
/**
* Delete upload
*/
async delete(req, res) {
const id = await this.getAndVerifyId(req, res);
const [file] = await this.storage.delete({ id });
const response = await this.storage.onDelete(file);
this.send(res, { statusCode: 204, ...response });
return file;
}
getId(req) {
const { query } = url_1.default.parse(req.url || '', true);
return (query.upload_id || query.prefix || super.getId(req));
}
buildHeaders(file, headers = {}) {
if (file.expiredAt)
headers['X-Upload-Expires'] = new Date(file.expiredAt).toISOString();
return headers;
}
/**
* Build file url from request
*/
buildFileUrl(req, file) {
if (file.GCSUploadURI)
return file.GCSUploadURI;
const { query, pathname } = url_1.default.parse(req.originalUrl || req.url, true);
query.upload_id = file.id;
const relative = url_1.default.format({ pathname, query });
return this.storage.config.useRelativeLocation ? relative : (0, utils_1.getBaseUrl)(req) + relative;
}
async getMetadata(req) {
const metadata = await (0, utils_1.getJsonBody)(req, this.storage.maxMetadataSize).catch(err => (0, utils_1.fail)(utils_1.ERRORS.BAD_REQUEST, err));
if (Object.keys(metadata).length)
return metadata;
const { query } = url_1.default.parse(decodeURI(req.url || ''), true);
return { ...query };
}
extractChecksum(req) {
const contentMD5 = (0, utils_1.getHeader)(req, 'content-md5');
if (contentMD5)
return { checksumAlgorithm: 'md5', checksum: contentMD5 };
const [type, checksum] = (0, utils_1.getHeader)(req, 'digest').split(/=(.*)/s);
return { checksumAlgorithm: { sha: 'sha1', 'sha-256': 'sha256' }[type] || type, checksum };
}
}
exports.Uploadx = Uploadx;
Uploadx.RESUME_STATUS_CODE = 308;
/**
* Basic express wrapper
* @example
* ```ts
* app.use('/files', uploadx({directory: '/tmp', maxUploadSize: '250GB'}));
* ```
*/
function uploadx(options = {}) {
return new Uploadx(options).handle;
}
/**
* Express wrapper
*
* - express ***should*** respond to the client when the upload complete and handle errors and GET requests
* @example
* ```ts
* app.use('/files', uploadx.upload({ storage }), (req, res, next) => {
* if (req.method === 'GET') return res.sendStatus(404);
* console.log('File upload complete: ', req.body.name);
* return res.json(req.body);
* });
* ```
*/
uploadx.upload = (options = {}) => new Uploadx(options).upload;