123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- 'use strict'
- const util = require('util')
- const pMap = require('p-map')
- const contentPath = require('./content/path')
- const fixOwner = require('./util/fix-owner')
- const fs = require('fs')
- const fsm = require('fs-minipass')
- const glob = util.promisify(require('glob'))
- const index = require('./entry-index')
- const path = require('path')
- const rimraf = util.promisify(require('rimraf'))
- const ssri = require('ssri')
- const hasOwnProperty = (obj, key) =>
- Object.prototype.hasOwnProperty.call(obj, key)
- const stat = util.promisify(fs.stat)
- const truncate = util.promisify(fs.truncate)
- const writeFile = util.promisify(fs.writeFile)
- const readFile = util.promisify(fs.readFile)
- const verifyOpts = (opts) => ({
- concurrency: 20,
- log: { silly () {} },
- ...opts,
- })
- module.exports = verify
- function verify (cache, opts) {
- opts = verifyOpts(opts)
- opts.log.silly('verify', 'verifying cache at', cache)
- const steps = [
- markStartTime,
- fixPerms,
- garbageCollect,
- rebuildIndex,
- cleanTmp,
- writeVerifile,
- markEndTime,
- ]
- return steps
- .reduce((promise, step, i) => {
- const label = step.name
- const start = new Date()
- return promise.then((stats) => {
- return step(cache, opts).then((s) => {
- s &&
- Object.keys(s).forEach((k) => {
- stats[k] = s[k]
- })
- const end = new Date()
- if (!stats.runTime)
- stats.runTime = {}
- stats.runTime[label] = end - start
- return Promise.resolve(stats)
- })
- })
- }, Promise.resolve({}))
- .then((stats) => {
- stats.runTime.total = stats.endTime - stats.startTime
- opts.log.silly(
- 'verify',
- 'verification finished for',
- cache,
- 'in',
- `${stats.runTime.total}ms`
- )
- return stats
- })
- }
- function markStartTime (cache, opts) {
- return Promise.resolve({ startTime: new Date() })
- }
- function markEndTime (cache, opts) {
- return Promise.resolve({ endTime: new Date() })
- }
- function fixPerms (cache, opts) {
- opts.log.silly('verify', 'fixing cache permissions')
- return fixOwner
- .mkdirfix(cache, cache)
- .then(() => {
- // TODO - fix file permissions too
- return fixOwner.chownr(cache, cache)
- })
- .then(() => null)
- }
- // Implements a naive mark-and-sweep tracing garbage collector.
- //
- // The algorithm is basically as follows:
- // 1. Read (and filter) all index entries ("pointers")
- // 2. Mark each integrity value as "live"
- // 3. Read entire filesystem tree in `content-vX/` dir
- // 4. If content is live, verify its checksum and delete it if it fails
- // 5. If content is not marked as live, rimraf it.
- //
- function garbageCollect (cache, opts) {
- opts.log.silly('verify', 'garbage collecting content')
- const indexStream = index.lsStream(cache)
- const liveContent = new Set()
- indexStream.on('data', (entry) => {
- if (opts.filter && !opts.filter(entry))
- return
- liveContent.add(entry.integrity.toString())
- })
- return new Promise((resolve, reject) => {
- indexStream.on('end', resolve).on('error', reject)
- }).then(() => {
- const contentDir = contentPath.contentDir(cache)
- return glob(path.join(contentDir, '**'), {
- follow: false,
- nodir: true,
- nosort: true,
- }).then((files) => {
- return Promise.resolve({
- verifiedContent: 0,
- reclaimedCount: 0,
- reclaimedSize: 0,
- badContentCount: 0,
- keptSize: 0,
- }).then((stats) =>
- pMap(
- files,
- (f) => {
- const split = f.split(/[/\\]/)
- const digest = split.slice(split.length - 3).join('')
- const algo = split[split.length - 4]
- const integrity = ssri.fromHex(digest, algo)
- if (liveContent.has(integrity.toString())) {
- return verifyContent(f, integrity).then((info) => {
- if (!info.valid) {
- stats.reclaimedCount++
- stats.badContentCount++
- stats.reclaimedSize += info.size
- } else {
- stats.verifiedContent++
- stats.keptSize += info.size
- }
- return stats
- })
- } else {
- // No entries refer to this content. We can delete.
- stats.reclaimedCount++
- return stat(f).then((s) => {
- return rimraf(f).then(() => {
- stats.reclaimedSize += s.size
- return stats
- })
- })
- }
- },
- { concurrency: opts.concurrency }
- ).then(() => stats)
- )
- })
- })
- }
- function verifyContent (filepath, sri) {
- return stat(filepath)
- .then((s) => {
- const contentInfo = {
- size: s.size,
- valid: true,
- }
- return ssri
- .checkStream(new fsm.ReadStream(filepath), sri)
- .catch((err) => {
- if (err.code !== 'EINTEGRITY')
- throw err
- return rimraf(filepath).then(() => {
- contentInfo.valid = false
- })
- })
- .then(() => contentInfo)
- })
- .catch((err) => {
- if (err.code === 'ENOENT')
- return { size: 0, valid: false }
- throw err
- })
- }
- function rebuildIndex (cache, opts) {
- opts.log.silly('verify', 'rebuilding index')
- return index.ls(cache).then((entries) => {
- const stats = {
- missingContent: 0,
- rejectedEntries: 0,
- totalEntries: 0,
- }
- const buckets = {}
- for (const k in entries) {
- /* istanbul ignore else */
- if (hasOwnProperty(entries, k)) {
- const hashed = index.hashKey(k)
- const entry = entries[k]
- const excluded = opts.filter && !opts.filter(entry)
- excluded && stats.rejectedEntries++
- if (buckets[hashed] && !excluded)
- buckets[hashed].push(entry)
- else if (buckets[hashed] && excluded) {
- // skip
- } else if (excluded) {
- buckets[hashed] = []
- buckets[hashed]._path = index.bucketPath(cache, k)
- } else {
- buckets[hashed] = [entry]
- buckets[hashed]._path = index.bucketPath(cache, k)
- }
- }
- }
- return pMap(
- Object.keys(buckets),
- (key) => {
- return rebuildBucket(cache, buckets[key], stats, opts)
- },
- { concurrency: opts.concurrency }
- ).then(() => stats)
- })
- }
- function rebuildBucket (cache, bucket, stats, opts) {
- return truncate(bucket._path).then(() => {
- // This needs to be serialized because cacache explicitly
- // lets very racy bucket conflicts clobber each other.
- return bucket.reduce((promise, entry) => {
- return promise.then(() => {
- const content = contentPath(cache, entry.integrity)
- return stat(content)
- .then(() => {
- return index
- .insert(cache, entry.key, entry.integrity, {
- metadata: entry.metadata,
- size: entry.size,
- })
- .then(() => {
- stats.totalEntries++
- })
- })
- .catch((err) => {
- if (err.code === 'ENOENT') {
- stats.rejectedEntries++
- stats.missingContent++
- return
- }
- throw err
- })
- })
- }, Promise.resolve())
- })
- }
- function cleanTmp (cache, opts) {
- opts.log.silly('verify', 'cleaning tmp directory')
- return rimraf(path.join(cache, 'tmp'))
- }
- function writeVerifile (cache, opts) {
- const verifile = path.join(cache, '_lastverified')
- opts.log.silly('verify', 'writing verifile to ' + verifile)
- try {
- return writeFile(verifile, '' + +new Date())
- } finally {
- fixOwner.chownr.sync(cache, verifile)
- }
- }
- module.exports.lastRun = lastRun
- function lastRun (cache) {
- return readFile(path.join(cache, '_lastverified'), 'utf8').then(
- (data) => new Date(+data)
- )
- }
|