UNPKG

@uploadx/core

Version:
190 lines (189 loc) 6.95 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseStorage = exports.locker = void 0; const bytes_1 = __importDefault(require("bytes")); const timers_1 = require("timers"); const util_1 = require("util"); const utils_1 = require("../utils"); const config_1 = require("./config"); const file_1 = require("./file"); const LOCK_TIMEOUT = 300; // seconds exports.locker = new utils_1.Locker(1000, LOCK_TIMEOUT); class BaseStorage { constructor(config) { this.config = config; this.isReady = true; this.checksumTypes = []; this.errorResponses = {}; this.validation = new utils_1.Validator(); const configHandler = new config_1.ConfigHandler(); const opts = configHandler.set(config); this.path = opts.path; this.onCreate = (0, utils_1.normalizeHookResponse)(opts.onCreate); this.onUpdate = (0, utils_1.normalizeHookResponse)(opts.onUpdate); this.onComplete = (0, utils_1.normalizeHookResponse)(opts.onComplete); this.onDelete = (0, utils_1.normalizeHookResponse)(opts.onDelete); this.onError = (0, utils_1.normalizeOnErrorResponse)(opts.onError); this.namingFunction = opts.filename; this.maxUploadSize = bytes_1.default.parse(opts.maxUploadSize); this.maxMetadataSize = bytes_1.default.parse(opts.maxMetadataSize); this.cache = new utils_1.Cache(1000, 300); this.logger = opts.logger; if (opts.logLevel && 'logLevel' in this.logger) { this.logger.logLevel = opts.logLevel; } this.logger.debug(`${this.constructor.name} config: ${(0, util_1.inspect)({ ...config, logger: this.logger.constructor })}`); const purgeInterval = (0, utils_1.toMilliseconds)(opts.expiration?.purgeInterval); if (purgeInterval) { this.startAutoPurge(purgeInterval); } const size = { value: this.maxUploadSize, isValid(file) { return file.size <= this.value; }, response: utils_1.ErrorMap.RequestEntityTooLarge }; const mime = { value: opts.allowMIME, isValid(file) { return !!utils_1.typeis.is(file.contentType, this.value); }, response: utils_1.ErrorMap.UnsupportedMediaType }; const filename = { isValid(file) { return file_1.FileName.isValid(file.name); }, response: utils_1.ErrorMap.InvalidFileName }; this.validation.add({ size, mime, filename }); this.validation.add({ ...opts.validation }); } async validate(file) { return this.validation.verify(file); } normalizeError(error) { return { message: 'Generic Uploadx Error', statusCode: 500, code: 'GenericUploadxError' }; } /** * Saves upload metadata */ async saveMeta(file) { this.updateTimestamps(file); const prev = { ...this.cache.get(file.id) }; this.cache.set(file.id, file); return (0, utils_1.isEqual)(prev, file, 'bytesWritten', 'expiredAt') ? this.meta.touch(file.id, file) : this.meta.save(file.id, file); } /** * Deletes an upload metadata */ async deleteMeta(id) { this.cache.delete(id); return this.meta.delete(id); } /** * Retrieves upload metadata */ async getMeta(id) { let file = this.cache.get(id); if (!file) { try { file = await this.meta.get(id); this.cache.set(file.id, file); } catch { return (0, utils_1.fail)(utils_1.ERRORS.FILE_NOT_FOUND); } } return { ...file }; } checkIfExpired(file) { if ((0, file_1.isExpired)(file)) { void this.delete(file).catch(() => null); return (0, utils_1.fail)(utils_1.ERRORS.GONE); } return Promise.resolve(file); } /** * Searches for and purges expired uploads * @param maxAge - remove uploads older than a specified age * @param prefix - filter uploads */ async purge(maxAge, prefix) { const maxAgeMs = (0, utils_1.toMilliseconds)(maxAge || this.config.expiration?.maxAge); const purged = { items: [], maxAgeMs, prefix }; if (maxAgeMs) { const before = Date.now() - maxAgeMs; const expired = (await this.list(prefix)).items.filter(item => +new Date(this.config.expiration?.rolling ? item.modifiedAt || item.createdAt : item.createdAt) < before); for (const { id, ...rest } of expired) { const [deleted] = await this.delete({ id }); purged.items.push({ ...deleted, ...rest }); } purged.items.length && this.logger.info(`Purge: removed ${purged.items.length} uploads`); } return purged; } async get({ id }) { return this.meta.list(id); } /** * Retrieves a list of uploads whose names begin with the prefix * @experimental */ async list(prefix = '') { return this.meta.list(prefix); } /** * Set user-provided metadata as key-value pairs * @experimental */ async update({ id }, metadata) { const file = await this.getMeta(id); (0, file_1.updateMetadata)(file, metadata); await this.saveMeta(file); return { ...file, status: 'updated' }; } /** * Prevent upload from being accessed by multiple requests */ async lock(key) { try { return exports.locker.lock(key); } catch { return (0, utils_1.fail)(utils_1.ERRORS.FILE_LOCKED); } } async unlock(key) { exports.locker.unlock(key); } isUnsupportedChecksum(algorithm = '') { return !!algorithm && !this.checksumTypes.includes(algorithm); } startAutoPurge(purgeInterval) { if (purgeInterval >= 2147483647) throw Error('“purgeInterval” must be less than 2147483647 ms'); (0, timers_1.setInterval)(() => void this.purge().catch(e => this.logger.error(e)), purgeInterval); } updateTimestamps(file) { file.createdAt ?? (file.createdAt = new Date().toISOString()); const maxAgeMs = (0, utils_1.toMilliseconds)(this.config.expiration?.maxAge); if (maxAgeMs) { file.expiredAt = this.config.expiration?.rolling ? new Date(Date.now() + maxAgeMs).toISOString() : new Date(+new Date(file.createdAt) + maxAgeMs).toISOString(); } return file; } } exports.BaseStorage = BaseStorage;