UNPKG

file-box

Version:

Pack a File into Box for easy move/transfer between servers no matter of where it is.(local path, remote url, or cloud storage)

259 lines 10.9 kB
"use strict"; /** * Huan(202110): Assignment and Resolution of Uniform Resource Names * https://datatracker.ietf.org/wg/urn/about/ */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.UniformResourceNameRegistry = void 0; /** * RFC 4122: A Universally Unique IDentifier (UUID) URN Namespace * ------------------------------------------------------------ * This specification defines a Uniform Resource Name namespace for * UUIDs (Universally Unique IDentifier), also known as GUIDs (Globally * Unique IDentifier). A UUID is 128 bits long, and can guarantee * uniqueness across space and time. UUIDs were originally used in the * Apollo Network Computing System and later in the Open Software * Foundation's (OSF) Distributed Computing Environment (DCE), and then * in Microsoft Windows platforms. * * The information here is meant to be a concise guide for those wishing * to implement services using UUIDs as URNs. Nothing in this document * should be construed to override the DCE standards that defined UUIDs. */ /** * RFC 2141: Uniform Resource Names (URNs) Syntax * ---------------------------------------------- * Uniform Resource Names (URNs) are intended to serve as persistent, * location-independent, resource identifiers. This document sets * forward the canonical syntax for URNs. A discussion of both existing * legacy and new namespaces and requirements for URN presentation and * transmission are presented. Finally, there is a discussion of URN * equivalence and how to determine it. */ const fs_1 = __importDefault(require("fs")); const os_1 = __importDefault(require("os")); const path_1 = __importDefault(require("path")); const clone_class_1 = require("clone-class"); const brolog_1 = require("brolog"); const file_box_js_1 = require("../file-box.js"); const random_uuid_js_1 = require("./random-uuid.js"); /** * A UUID will be only keep for a certain time. */ const DEFAULT_UUID_EXPIRE_MINUTES = 30; const DEFAULT_UUID_PURGE_INTERVAL_MINUTES = 1; class UniformResourceNameRegistry { static processExitMap = new WeakMap(); /** * The directory that store all UUID files */ storeDir; expireMilliseconds; /** * Key: expiretime * Value: the array of UUID that will expires after the `expiretime` (Key) */ uuidExpiringTable; purgerTimer; constructor(options = {}) { brolog_1.log.verbose('UniformResourceNameRegistry', 'constructor("%s")', JSON.stringify(options)); this.uuidExpiringTable = new Map(); this.expireMilliseconds = options.expireMilliseconds ?? (DEFAULT_UUID_EXPIRE_MINUTES * 60 * 1000 * 1000); this.storeDir = options.storeDir || path_1.default.join(os_1.default.tmpdir(), 'file-box-urn-registry.' + String(process.pid)); } /** * Return a FileBox Interface with the current URN Registry for conience */ getFileBox() { this.init(); class UUIDFileBox extends file_box_js_1.FileBox { } UUIDFileBox.setUuidLoader(this.load.bind(this)); UUIDFileBox.setUuidSaver(this.save.bind(this)); return UUIDFileBox; } /** * init the UUID registry * * must be called before use. */ init() { brolog_1.log.verbose('UniformResourceNameRegistry', 'init()'); try { const stat = fs_1.default.statSync(this.storeDir); if (!stat.isDirectory()) { throw new Error(this.storeDir + ' is Not a directory'); } } catch (e) { if (e.code === 'ENOENT') { fs_1.default.mkdirSync(this.storeDir, { recursive: true }); } else { throw e; } } if (!this.purgerTimer) { this.addProcessExitListener(); this.purgerTimer = setInterval(() => this.purgeExpiredUuid(), DEFAULT_UUID_PURGE_INTERVAL_MINUTES * 60 * 1000); } } purgeExpiredUuid() { brolog_1.log.verbose('UniformResourceNameRegistry', 'purgeExpiredUuid()'); const expireTimeList = [...this.uuidExpiringTable.keys()] .sort((a, b) => Number(a) - Number(b)); for (const expireTime of expireTimeList) { if (Date.now() < expireTime) { // The earliest expire time is in the future break; } const uuidList = this.uuidExpiringTable.get(expireTime) || []; this.uuidExpiringTable.delete(expireTime); for (const uuid of uuidList) { this.purge(uuid).catch(console.error); } } } /** * Clean up by calling this.destroy() before process exit */ addProcessExitListener() { brolog_1.log.verbose('UniformResourceNameRegistry', 'addProcessExitListener()'); const Klass = (0, clone_class_1.instanceToClass)(this, UniformResourceNameRegistry); /** * If we have already registered the listener, do nothing. */ if (Klass.processExitMap.has(this)) { return; } const destroyCallback = () => this.destroy(); process.addListener('exit', destroyCallback); Klass.processExitMap.set(this, () => process.removeListener('exit', destroyCallback)); } uuidFileName(uuid) { return path_1.default.join(this.storeDir, uuid + '.dat'); } /** * @deprecated use `load()` instead */ async resolve(uuid) { brolog_1.log.warn('UniformResourceNameRegistry', 'resolve() is deprecated: use `load()` instead.\n%s', new Error().stack); return this.load(uuid); } /** * `resolve()` can only be used once. * after resolve(), the UUID will be not exist any more */ async load(uuid) { brolog_1.log.verbose('UniformResourceNameRegistry', 'load(%s)', uuid); const filename = this.uuidFileName(uuid); const stream = fs_1.default.createReadStream(filename); await new Promise((resolve, reject) => { stream.on('ready', resolve); stream.on('error', reject); }); return stream; } /** * @deprecated use `save()` instead */ async register(stream) { brolog_1.log.verbose('UniformResourceNameRegistry', 'register() deprecated: use save() instead.\n%s', new Error().stack); return this.save(stream); } /** * Save the `Readable` stream and return a random UUID * The UUID will be expired after MAX_KEEP_MINUTES */ async save(stream) { brolog_1.log.verbose('UniformResourceNameRegistry', 'save(stream)'); const uuid = (0, random_uuid_js_1.randomUuid)(); const fileStream = fs_1.default.createWriteStream(this.uuidFileName(uuid)); const future = new Promise((resolve, reject) => { stream.on('end', resolve); stream.on('error', reject); fileStream.on('error', reject); }); stream.pipe(fileStream); await future; this.addToExpiringTable(uuid); return uuid; } /** * Set a timer to execute delete callback after `expireMilliseconds` */ addToExpiringTable(uuid) { brolog_1.log.verbose('UniformResourceNameRegistry', 'addToExpiringTable(%s)', uuid); const expireTime = Date.now() + this.expireMilliseconds; const expireTimeInterval = DEFAULT_UUID_PURGE_INTERVAL_MINUTES * 60 * 1000; // https://stackoverflow.com/a/22687090 const expireTimeNearestMinute = Math.ceil(expireTime / expireTimeInterval) * expireTimeInterval; const uuidList = this.uuidExpiringTable.get(expireTime) || []; uuidList.push(uuid); this.uuidExpiringTable.set(expireTimeNearestMinute, uuidList); brolog_1.log.silly('UniformResourceNameRegistry', 'addToExpiringTable() uuidList.length = %s, expireTime = %s', uuidList.length, expireTimeNearestMinute); } async purge(uuid) { brolog_1.log.verbose('UniformResourceNameRegistry', 'purge(%s)', uuid); const file = this.uuidFileName(uuid); try { await fs_1.default.promises.unlink(file); brolog_1.log.silly('UniformResourceNameRegistry', 'purge() %s', file); } catch (e) { brolog_1.log.warn('UniformResourceNameRegistry', 'purge() rejection:', e.message); } } /** * destroy the urn registry. * * This function will be called automatically at `process.on(exit)` * however, it till need to be called before the program ends * because there have some timers in eventloop task list */ destroy() { brolog_1.log.verbose('UniformResourceNameRegistry', 'destroy() %s UUIDs left', [...this.uuidExpiringTable.values()].flat().length); if (this.purgerTimer) { brolog_1.log.verbose('UniformResourceNameRegistry', 'destroy() clearing purger timer ...'); clearInterval(this.purgerTimer); this.purgerTimer = undefined; } const Klass = (0, clone_class_1.instanceToClass)(this, UniformResourceNameRegistry); /** * Remove process exit listener */ if (Klass.processExitMap.has(this)) { brolog_1.log.verbose('UniformResourceNameRegistry', 'destroy() remove process `exit` listener ...'); const fn = Klass.processExitMap.get(this); Klass.processExitMap.delete(this); fn && fn(); } /** * Clean up all the files */ brolog_1.log.verbose('UniformResourceNameRegistry', 'destroy() fs.rmSync(%s) ...', this.storeDir); try { /** * Huan(202110): * Check for the `this.uuidDir` exist or not * when we are running unit tests, we might instanciate multiple UniformResourceNameRegistry * which will cause the `this.destroy()` to be registered multiple times */ fs_1.default.statSync(this.storeDir); fs_1.default.rmSync(this.storeDir, { recursive: true }); brolog_1.log.verbose('UniformResourceNameRegistry', 'destroy() fs.rmSync(%s) done', this.storeDir); } catch (e) { if (e.code === 'ENOENT') { brolog_1.log.verbose('UniformResourceNameRegistry', 'destroy() %s not exist', this.storeDir); return; } brolog_1.log.warn('UniformResourceNameRegistry', 'destroy() fs.rmSync(%s) exception: %s', e.message); } } } exports.UniformResourceNameRegistry = UniformResourceNameRegistry; //# sourceMappingURL=uniform-resource-name-registry.js.map