import fsSyncOriginal from 'fs' import fsPromisesOriginal from 'fs/promises' import crypto from 'crypto' import path from 'path' import os from 'os' export default class FSCache { constructor(options = {}, fsSync, fsPromises) { this.fsSync = fsSync || fsSyncOriginal this.fsPromises = fsPromises || fsPromisesOriginal this.id = crypto.randomBytes(15).toString('base64').replace(/\//g, '-') this.prefix = options.prefix ? options.prefix + '-' : '-' this.hash_alg = options.hash_alg || 'md5' this.cache_dir = options.cache_dir || path.join(os.tmpdir(), this.id) this.ttl = options.ttl || 0 // Verify hash algorithm is supported on this system crypto.createHash(this.hash_alg) this.fsSync.mkdirSync(this.cache_dir, { recursive: true }) } _checkIsExpired(parsed, now) { return parsed.ttl != null && now > parsed.ttl } _parseCacheData(data, fallback, overwrite = {}) { let parsed = JSON.parse(data) if (this._checkIsExpired(parsed, new Date().getTime())) { return fallback || null } return parsed.content } _parseSetData(key, data, overwrite = {}) { if (!(overwrite.ttl ?? this.ttl)) { return JSON.stringify({ key: key, content: data }) } return JSON.stringify({ key: key, content: data, ttl: new Date().getTime() + (overwrite.ttl || this.ttl) * 1000, }) } hash(name) { return path.join(this.cache_dir, this.prefix + crypto.hash(this.hash_alg, name)) } get(name, fallback, opts) { return this.fsPromises.readFile(this.hash(name), { encoding: 'utf8' }) .then( data => this._parseCacheData(data, fallback, opts), err => (fallback) ) } getSync(name, fallback, opts) { let data; try { data = this.fsSync.readFileSync(this.hash(name), { encoding: 'utf8' }) } catch { return fallback } return this._parseCacheData(data, fallback, opts) } set(name, data, orgOpts = {}) { let opts = typeof orgOpts === 'number' ? { ttl: orgOpts } : orgOpts try { return this.fsPromises.writeFile( this.hash(name), this._parseSetData(name, data, opts), { encoding: opts.encoding || 'utf8' } ) } catch (err) { return Promise.reject(err) } } async setMany(items, options) { for (let item of items) { await this.set(item.key, item.content ?? item.value, options) } } save(items, options) { return this.setMany(items, options) } setSync(name, data, orgOpts = {}) { let opts = typeof orgOpts === 'number' ? { ttl: orgOpts } : orgOpts this.fsSync.writeFileSync( this.hash(name), this._parseSetData(name, data, opts), { encoding: opts.encoding || 'utf8' } ) } setManySync(items, options) { for (let item of items) { this.setSync(item.key, item.content ?? item.value, options) } } saveSync(items, options) { return this.setManySync(items, options) } remove(name) { return this.fsPromises.rm(this.hash(name), { force: true }) } removeSync(name) { return this.fsSync.rmSync(this.hash(name), { force: true }) } async clear() { for (let file of await this.fsPromises.readdir(this.cache_dir)) { if (!file.startsWith(this.prefix)) continue await this.fsPromises.rm(path.join(this.cache_dir, file), { force: true }) } } clearSync() { for (let file of this.fsSync.readdirSync(this.cache_dir)) { if (!file.startsWith(this.prefix)) continue this.fsSync.rmSync(path.join(this.cache_dir, file), { force: true }) } } async getAll() { let out = [] let now = new Date().getTime() for (let file of await this.fsPromises.readdir(this.cache_dir)) { if (!file.startsWith(this.prefix)) continue let data = await this.fsPromises.readFile(path.join(this.cache_dir, file), { encoding: 'utf8' }) let entry = JSON.parse(data) if (entry.content && !this._checkIsExpired(entry, now)) { out.push(entry) } } return out } load() { return this.getAll().then(res => { return { files: res.map(entry => ({ path: this.hash(entry.key), value: entry.content, key: entry.key })) } }) } }