UNPKG

@uploadx/core

Version:
196 lines (195 loc) 7.41 kB
"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'];