|
|
- 'use strict'
-
- const BB = require('bluebird')
-
- const contentPath = require('./content/path')
- const crypto = require('crypto')
- const figgyPudding = require('figgy-pudding')
- const fixOwner = require('./util/fix-owner')
- const fs = require('graceful-fs')
- const hashToSegments = require('./util/hash-to-segments')
- const ms = require('mississippi')
- const path = require('path')
- const ssri = require('ssri')
- const Y = require('./util/y.js')
-
- const indexV = require('../package.json')['cache-version'].index
-
- const appendFileAsync = BB.promisify(fs.appendFile)
- const readFileAsync = BB.promisify(fs.readFile)
- const readdirAsync = BB.promisify(fs.readdir)
- const concat = ms.concat
- const from = ms.from
-
- module.exports.NotFoundError = class NotFoundError extends Error {
- constructor (cache, key) {
- super(Y`No cache entry for \`${key}\` found in \`${cache}\``)
- this.code = 'ENOENT'
- this.cache = cache
- this.key = key
- }
- }
-
- const IndexOpts = figgyPudding({
- metadata: {},
- size: {}
- })
-
- module.exports.insert = insert
- function insert (cache, key, integrity, opts) {
- opts = IndexOpts(opts)
- const bucket = bucketPath(cache, key)
- const entry = {
- key,
- integrity: integrity && ssri.stringify(integrity),
- time: Date.now(),
- size: opts.size,
- metadata: opts.metadata
- }
- return fixOwner.mkdirfix(
- cache, path.dirname(bucket)
- ).then(() => {
- const stringified = JSON.stringify(entry)
- // NOTE - Cleverness ahoy!
- //
- // This works because it's tremendously unlikely for an entry to corrupt
- // another while still preserving the string length of the JSON in
- // question. So, we just slap the length in there and verify it on read.
- //
- // Thanks to @isaacs for the whiteboarding session that ended up with this.
- return appendFileAsync(
- bucket, `\n${hashEntry(stringified)}\t${stringified}`
- )
- }).then(
- () => fixOwner.chownr(cache, bucket)
- ).catch({ code: 'ENOENT' }, () => {
- // There's a class of race conditions that happen when things get deleted
- // during fixOwner, or between the two mkdirfix/chownr calls.
- //
- // It's perfectly fine to just not bother in those cases and lie
- // that the index entry was written. Because it's a cache.
- }).then(() => {
- return formatEntry(cache, entry)
- })
- }
-
- module.exports.insert.sync = insertSync
- function insertSync (cache, key, integrity, opts) {
- opts = IndexOpts(opts)
- const bucket = bucketPath(cache, key)
- const entry = {
- key,
- integrity: integrity && ssri.stringify(integrity),
- time: Date.now(),
- size: opts.size,
- metadata: opts.metadata
- }
- fixOwner.mkdirfix.sync(cache, path.dirname(bucket))
- const stringified = JSON.stringify(entry)
- fs.appendFileSync(
- bucket, `\n${hashEntry(stringified)}\t${stringified}`
- )
- try {
- fixOwner.chownr.sync(cache, bucket)
- } catch (err) {
- if (err.code !== 'ENOENT') {
- throw err
- }
- }
- return formatEntry(cache, entry)
- }
-
- module.exports.find = find
- function find (cache, key) {
- const bucket = bucketPath(cache, key)
- return bucketEntries(bucket).then(entries => {
- return entries.reduce((latest, next) => {
- if (next && next.key === key) {
- return formatEntry(cache, next)
- } else {
- return latest
- }
- }, null)
- }).catch(err => {
- if (err.code === 'ENOENT') {
- return null
- } else {
- throw err
- }
- })
- }
-
- module.exports.find.sync = findSync
- function findSync (cache, key) {
- const bucket = bucketPath(cache, key)
- try {
- return bucketEntriesSync(bucket).reduce((latest, next) => {
- if (next && next.key === key) {
- return formatEntry(cache, next)
- } else {
- return latest
- }
- }, null)
- } catch (err) {
- if (err.code === 'ENOENT') {
- return null
- } else {
- throw err
- }
- }
- }
-
- module.exports.delete = del
- function del (cache, key, opts) {
- return insert(cache, key, null, opts)
- }
-
- module.exports.delete.sync = delSync
- function delSync (cache, key, opts) {
- return insertSync(cache, key, null, opts)
- }
-
- module.exports.lsStream = lsStream
- function lsStream (cache) {
- const indexDir = bucketDir(cache)
- const stream = from.obj()
-
- // "/cachename/*"
- readdirOrEmpty(indexDir).map(bucket => {
- const bucketPath = path.join(indexDir, bucket)
-
- // "/cachename/<bucket 0xFF>/*"
- return readdirOrEmpty(bucketPath).map(subbucket => {
- const subbucketPath = path.join(bucketPath, subbucket)
-
- // "/cachename/<bucket 0xFF>/<bucket 0xFF>/*"
- return readdirOrEmpty(subbucketPath).map(entry => {
- const getKeyToEntry = bucketEntries(
- path.join(subbucketPath, entry)
- ).reduce((acc, entry) => {
- acc.set(entry.key, entry)
- return acc
- }, new Map())
-
- return getKeyToEntry.then(reduced => {
- for (let entry of reduced.values()) {
- const formatted = formatEntry(cache, entry)
- formatted && stream.push(formatted)
- }
- }).catch({ code: 'ENOENT' }, nop)
- })
- })
- }).then(() => {
- stream.push(null)
- }, err => {
- stream.emit('error', err)
- })
-
- return stream
- }
-
- module.exports.ls = ls
- function ls (cache) {
- return BB.fromNode(cb => {
- lsStream(cache).on('error', cb).pipe(concat(entries => {
- cb(null, entries.reduce((acc, xs) => {
- acc[xs.key] = xs
- return acc
- }, {}))
- }))
- })
- }
-
- function bucketEntries (bucket, filter) {
- return readFileAsync(
- bucket, 'utf8'
- ).then(data => _bucketEntries(data, filter))
- }
-
- function bucketEntriesSync (bucket, filter) {
- const data = fs.readFileSync(bucket, 'utf8')
- return _bucketEntries(data, filter)
- }
-
- function _bucketEntries (data, filter) {
- let entries = []
- data.split('\n').forEach(entry => {
- if (!entry) { return }
- const pieces = entry.split('\t')
- if (!pieces[1] || hashEntry(pieces[1]) !== pieces[0]) {
- // Hash is no good! Corruption or malice? Doesn't matter!
- // EJECT EJECT
- return
- }
- let obj
- try {
- obj = JSON.parse(pieces[1])
- } catch (e) {
- // Entry is corrupted!
- return
- }
- if (obj) {
- entries.push(obj)
- }
- })
- return entries
- }
-
- module.exports._bucketDir = bucketDir
- function bucketDir (cache) {
- return path.join(cache, `index-v${indexV}`)
- }
-
- module.exports._bucketPath = bucketPath
- function bucketPath (cache, key) {
- const hashed = hashKey(key)
- return path.join.apply(path, [bucketDir(cache)].concat(
- hashToSegments(hashed)
- ))
- }
-
- module.exports._hashKey = hashKey
- function hashKey (key) {
- return hash(key, 'sha256')
- }
-
- module.exports._hashEntry = hashEntry
- function hashEntry (str) {
- return hash(str, 'sha1')
- }
-
- function hash (str, digest) {
- return crypto
- .createHash(digest)
- .update(str)
- .digest('hex')
- }
-
- function formatEntry (cache, entry) {
- // Treat null digests as deletions. They'll shadow any previous entries.
- if (!entry.integrity) { return null }
- return {
- key: entry.key,
- integrity: entry.integrity,
- path: contentPath(cache, entry.integrity),
- size: entry.size,
- time: entry.time,
- metadata: entry.metadata
- }
- }
-
- function readdirOrEmpty (dir) {
- return readdirAsync(dir)
- .catch({ code: 'ENOENT' }, () => [])
- .catch({ code: 'ENOTDIR' }, () => [])
- }
-
- function nop () {
- }
|