@uploadx/core
Version:
Node.js resumable upload middleware
190 lines (189 loc) • 6.95 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.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;