file-cache
Version:
Read file with LRU caching, rely on thunks.
177 lines (152 loc) • 5.73 kB
JavaScript
'use strict'
// **Github:** https://github.com/thunks/file-cache
//
// **License:** MIT
const fs = require('fs')
const zlib = require('zlib')
const path = require('path')
const crypto = require('crypto')
const mime = require('mime-types')
const compressible = require('compressible')
const LRUCache = require('lrucache')
const thunk = require('thunks')()
const stat = thunk.thunkify(fs.stat)
const readFile = thunk.thunkify(fs.readFile)
const compressFile = {
gzip: thunk.thunkify(zlib.gzip),
deflate: thunk.thunkify(zlib.deflate),
origin: function (buf) {
return thunk(buf)
}
}
module.exports = function (options) {
const cache = Object.create(null)
const extraFilesMap = Object.create(null)
const lruCache = new LRUCache()
const cwd = process.cwd()
let totalBytes = 0
options = options || {}
if (typeof options === 'string') options = {root: options}
// for test.
if (options.debug) module.exports.cache = cache
let root = typeof options.root === 'string' ? options.root : cwd
root = path.resolve(cwd, root)
const extraFiles = options.extraFiles || []
if (!Array.isArray(extraFiles)) throw new Error('"extraFiles" must be Array')
for (let i = 0; i < extraFiles.length; i++) {
extraFilesMap[extraFiles[i]] = path.resolve(cwd, extraFiles[i])
}
const enableCompress = options.compress !== false
const maxCacheLength = options.maxCacheLength >= -1 ? Math.floor(options.maxCacheLength) : 0
const minCompressLength = options.minCompressLength >= 16 ? Math.floor(options.minCompressLength) : 256
const md5Encoding = options.md5Encoding || 'base64'
function resolvePath (filePath) {
filePath = path.normalize(filePath)
if (extraFilesMap[filePath]) return extraFilesMap[filePath]
return path.join(root, path.join('/', filePath))
}
function checkLRU (filePath, addSize) {
if (maxCacheLength <= 0) return
totalBytes += addSize
lruCache.set(filePath, (lruCache.get(filePath) || 0) + addSize)
while (lruCache.staleKey() !== filePath && totalBytes >= maxCacheLength) {
let stale = lruCache.popStale()
delete cache[stale[0]]
totalBytes -= stale[1]
}
}
return function fileCache (filePath, encodings) {
return thunk(function (done) {
let compressEncoding = bestCompress(encodings)
filePath = resolvePath(filePath)
let readAndCacheFile = cache[filePath]
if (readAndCacheFile instanceof OriginFile) {
return cloneFile(readAndCacheFile, compressEncoding, checkLRU)(done)
}
if (!readAndCacheFile) {
readAndCacheFile = thunk.persist(thunk.seq([
stat(filePath),
readFile(filePath)
]))
if (maxCacheLength !== -1) cache[filePath] = readAndCacheFile
}
readAndCacheFile(function (error, res) {
if (error) throw error
let originFile = cache[filePath]
if (!(originFile instanceof OriginFile)) {
originFile = new OriginFile(filePath, res[0], res[1],
enableCompress, minCompressLength, md5Encoding)
cache[filePath] = maxCacheLength === -1 ? null : originFile
}
return cloneFile(originFile, compressEncoding, checkLRU)
})(done)
})
}
}
const enableEncodings = Object.create(null)
enableEncodings.gzip = true
enableEncodings.deflate = true
function bestCompress (encodings) {
if (!Array.isArray(encodings)) encodings = [encodings]
for (let i = 0; i < encodings.length; i++) {
if (enableEncodings[encodings[i]]) return encodings[i]
}
return 'origin'
}
function File (originFile, compressEncoding) {
this.md5 = originFile.md5
this.dir = originFile.dir
this.ext = originFile.ext
this.name = originFile.name
this.path = originFile.path
this.size = originFile.size
this.type = originFile.type
this.atime = originFile.atime
this.mtime = originFile.mtime
this.ctime = originFile.ctime
this.compress = compressEncoding === 'origin' ? '' : compressEncoding
this.contents = Buffer.from(originFile[compressEncoding])
}
function OriginFile (filePath, stats, buf, enableCompress, minCompressLength, md5Encoding) {
filePath = filePath.replace(/\\/g, '/')
this.count = 0
this.md5 = crypto.createHash('md5').update(buf).digest(md5Encoding)
this.dir = path.dirname(filePath)
this.ext = path.extname(filePath)
this.name = path.basename(filePath)
this.path = filePath
this.size = stats.size
this.type = mime.lookup(filePath) || 'application/octet-stream'
this.atime = stats.atime.toUTCString()
this.mtime = stats.mtime.toUTCString()
this.ctime = stats.ctime.toUTCString()
this.compressible = enableCompress && this.size > minCompressLength && compressible(this.type)
this.contents = buf
this.origin = null
this.gzip = null
this.deflate = null
}
function cloneFile (originFile, compressEncoding, checkLRU) {
return thunk(function (done) {
originFile.count += 1
if (!originFile.compressible) compressEncoding = 'origin'
let compressAndCacheBuffer = originFile[compressEncoding]
if (Buffer.isBuffer(compressAndCacheBuffer)) {
return done(null, new File(originFile, compressEncoding))
}
if (!compressAndCacheBuffer) {
compressAndCacheBuffer = thunk.persist(compressFile[compressEncoding](originFile.contents))
originFile[compressEncoding] = compressAndCacheBuffer
}
compressAndCacheBuffer(function (error, buf) {
if (error) throw error
if (!Buffer.isBuffer(originFile[compressEncoding])) {
originFile[compressEncoding] = buf
checkLRU(originFile.path, buf.length)
}
return new File(originFile, compressEncoding)
})(done)
})
}
module.exports.File = File
module.exports.OriginFile = OriginFile