@uploadx/core
Version:
Node.js resumable upload middleware
196 lines (195 loc) • 7.41 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.BaseHandler = void 0;
const events_1 = require("events");
const url_1 = __importDefault(require("url"));
const storages_1 = require("../storages");
const utils_1 = require("../utils");
const cors_1 = require("./cors");
class BaseHandler extends events_1.EventEmitter {
constructor(config = {}) {
super();
this.responseType = 'json';
this.registeredHandlers = new Map();
this._errorResponses = {};
this.handle = (req, res) => this.upload(req, res);
this.upload = (req, res, next) => {
if (this.cors.preflight(req, res)) {
res.writeHead(204, { 'Content-Length': 0 }).end();
return;
}
req.on('error', err => this.logger.error('[request error]: %O', err));
this.logger.debug('[request]: %s %s', req.method, req.url);
const handler = this.registeredHandlers.get(req.method);
if (!handler) {
return this.sendError(res, { uploadxErrorCode: utils_1.ERRORS.METHOD_NOT_ALLOWED });
}
if (!this.storage.isReady) {
return this.sendError(res, { uploadxErrorCode: utils_1.ERRORS.STORAGE_ERROR });
}
handler
.call(this, req, res)
.then(async (file) => {
if ('status' in file && file.status) {
this.logger.debug('[%s]: %s: %d/%d', file.status, file.name, file.bytesWritten, file.size);
this.listenerCount(file.status) &&
this.emit(file.status, { ...file, request: (0, utils_1.pick)(req, ['headers', 'method', 'url']) });
if (file.status === 'completed') {
if (next) {
req['_body'] = true;
req['body'] = file;
next();
}
else {
const completed = await this.storage.onComplete(file);
this.finish(req, res, completed);
}
}
return;
}
if (req.method === 'GET') {
req['body'] = file;
next ? next() : this.send(res, { body: file });
}
return;
})
.catch((error) => {
const err = (0, utils_1.pick)(error, [
'name',
...Object.getOwnPropertyNames(error)
]);
const errorEvent = { ...err, request: (0, utils_1.pick)(req, ['headers', 'method', 'url']) };
this.listenerCount('error') && this.emit('error', errorEvent);
this.logger.error('[error]: %O', errorEvent);
if ('aborted' in req && req['aborted'])
return;
return this.sendError(res, error);
});
};
this.getUserId = (req, _res) => req.user?.id || req.user?._id; // eslint-disable-line
this.cors = new cors_1.Cors();
this.storage =
'storage' in config
? config.storage
: new storages_1.DiskStorage(config);
if (config.userIdentifier) {
this.getUserId = config.userIdentifier;
}
this.logger = this.storage.logger;
this.assembleErrors();
this.compose();
}
/**
* Override error responses
* @example
* ```ts
* const uploadx = new Uploadx({ storage });
* uploadx.errorResponses = {
* FileNotFound: [404, { message: 'Not Found!' }]
* }
* ```
*/
set errorResponses(value) {
this.assembleErrors(value);
}
compose() {
const child = this.constructor;
(child.methods || BaseHandler.methods).forEach(method => {
const handler = this[method];
handler && this.registeredHandlers.set(method.toUpperCase(), handler);
// handler && this.cors.allowedMethods.push(method.toUpperCase());
});
this.logger.debug('Registered handlers: %s', [...this.registeredHandlers.keys()].join(', '));
}
assembleErrors(customErrors = {}) {
this._errorResponses = {
...utils_1.ErrorMap,
...this._errorResponses,
...this.storage.errorResponses,
...customErrors
};
}
async options(req, res) {
this.send(res, { statusCode: 204 });
return {};
}
/**
* Returns user uploads list
*/
get(req, res) {
const userId = this.getUserId(req, res);
return userId ? this.storage.list((0, utils_1.hash)(userId)) : (0, utils_1.fail)(utils_1.ERRORS.FILE_NOT_FOUND);
}
/**
* Make response
*/
send(res, { statusCode = 200, headers = {}, body = '' }) {
(0, utils_1.setHeaders)(res, headers);
let data;
if (typeof body !== 'string') {
data = JSON.stringify(body);
if (!headers['Content-Type'])
res.setHeader('Content-Type', 'application/json');
}
else {
data = body;
}
res.setHeader('Content-Length', Buffer.byteLength(data));
res.setHeader('Cache-Control', 'no-store');
res.writeHead(statusCode);
res.end(data);
}
/**
* Send Error to client
*/
sendError(res, error) {
const httpError = (0, utils_1.isUploadxError)(error)
? this._errorResponses[error.uploadxErrorCode]
: (0, utils_1.isValidationError)(error)
? error
: this.storage.normalizeError(error);
const response = this.storage.onError(httpError);
this.send(res, response);
}
/**
* Get id from request
*/
getId(req) {
const pathname = url_1.default.parse(req.url).pathname || '';
const path = req.originalUrl
? `/${pathname}`.replace('//', '')
: `/${pathname}`.replace(`/${this.storage.path}/`, '');
return path.startsWith('/') ? '' : path;
}
async getAndVerifyId(req, res) {
const uid = this.getUserId(req, res) || '';
const id = this.getId(req);
if (id && id.startsWith(uid && (0, utils_1.hash)(uid)))
return id;
return (0, utils_1.fail)(utils_1.ERRORS.FORBIDDEN);
}
/**
* Build file url from request
*/
buildFileUrl(req, file) {
const { query, pathname = '' } = url_1.default.parse(req.originalUrl || req.url, true);
const relative = url_1.default.format({ pathname: `${pathname}/${file.id}`, query });
return this.storage.config.useRelativeLocation ? relative : (0, utils_1.getBaseUrl)(req) + relative;
}
finish(req, res, response) {
return this.send(res, response);
}
}
exports.BaseHandler = BaseHandler;
/**
* Limiting enabled http method handlers
* @example
* ```ts
* Uploadx.methods = ['post', 'put', 'delete'];
* app.use('/upload', uploadx(opts));
* ```
*/
BaseHandler.methods = ['delete', 'get', 'head', 'options', 'patch', 'post', 'put'];