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
JavaScript
"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