157 lines
4.1 KiB
JavaScript
157 lines
4.1 KiB
JavaScript
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.ns || 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 }))
|
|
}
|
|
})
|
|
}
|
|
}
|