11cb0ef41Sopenharmony_ci'use strict' 21cb0ef41Sopenharmony_ci 31cb0ef41Sopenharmony_ciconst crypto = require('crypto') 41cb0ef41Sopenharmony_ciconst { 51cb0ef41Sopenharmony_ci appendFile, 61cb0ef41Sopenharmony_ci mkdir, 71cb0ef41Sopenharmony_ci readFile, 81cb0ef41Sopenharmony_ci readdir, 91cb0ef41Sopenharmony_ci rm, 101cb0ef41Sopenharmony_ci writeFile, 111cb0ef41Sopenharmony_ci} = require('fs/promises') 121cb0ef41Sopenharmony_ciconst { Minipass } = require('minipass') 131cb0ef41Sopenharmony_ciconst path = require('path') 141cb0ef41Sopenharmony_ciconst ssri = require('ssri') 151cb0ef41Sopenharmony_ciconst uniqueFilename = require('unique-filename') 161cb0ef41Sopenharmony_ci 171cb0ef41Sopenharmony_ciconst contentPath = require('./content/path') 181cb0ef41Sopenharmony_ciconst hashToSegments = require('./util/hash-to-segments') 191cb0ef41Sopenharmony_ciconst indexV = require('../package.json')['cache-version'].index 201cb0ef41Sopenharmony_ciconst { moveFile } = require('@npmcli/fs') 211cb0ef41Sopenharmony_ci 221cb0ef41Sopenharmony_cimodule.exports.NotFoundError = class NotFoundError extends Error { 231cb0ef41Sopenharmony_ci constructor (cache, key) { 241cb0ef41Sopenharmony_ci super(`No cache entry for ${key} found in ${cache}`) 251cb0ef41Sopenharmony_ci this.code = 'ENOENT' 261cb0ef41Sopenharmony_ci this.cache = cache 271cb0ef41Sopenharmony_ci this.key = key 281cb0ef41Sopenharmony_ci } 291cb0ef41Sopenharmony_ci} 301cb0ef41Sopenharmony_ci 311cb0ef41Sopenharmony_cimodule.exports.compact = compact 321cb0ef41Sopenharmony_ci 331cb0ef41Sopenharmony_ciasync function compact (cache, key, matchFn, opts = {}) { 341cb0ef41Sopenharmony_ci const bucket = bucketPath(cache, key) 351cb0ef41Sopenharmony_ci const entries = await bucketEntries(bucket) 361cb0ef41Sopenharmony_ci const newEntries = [] 371cb0ef41Sopenharmony_ci // we loop backwards because the bottom-most result is the newest 381cb0ef41Sopenharmony_ci // since we add new entries with appendFile 391cb0ef41Sopenharmony_ci for (let i = entries.length - 1; i >= 0; --i) { 401cb0ef41Sopenharmony_ci const entry = entries[i] 411cb0ef41Sopenharmony_ci // a null integrity could mean either a delete was appended 421cb0ef41Sopenharmony_ci // or the user has simply stored an index that does not map 431cb0ef41Sopenharmony_ci // to any content. we determine if the user wants to keep the 441cb0ef41Sopenharmony_ci // null integrity based on the validateEntry function passed in options. 451cb0ef41Sopenharmony_ci // if the integrity is null and no validateEntry is provided, we break 461cb0ef41Sopenharmony_ci // as we consider the null integrity to be a deletion of everything 471cb0ef41Sopenharmony_ci // that came before it. 481cb0ef41Sopenharmony_ci if (entry.integrity === null && !opts.validateEntry) { 491cb0ef41Sopenharmony_ci break 501cb0ef41Sopenharmony_ci } 511cb0ef41Sopenharmony_ci 521cb0ef41Sopenharmony_ci // if this entry is valid, and it is either the first entry or 531cb0ef41Sopenharmony_ci // the newEntries array doesn't already include an entry that 541cb0ef41Sopenharmony_ci // matches this one based on the provided matchFn, then we add 551cb0ef41Sopenharmony_ci // it to the beginning of our list 561cb0ef41Sopenharmony_ci if ((!opts.validateEntry || opts.validateEntry(entry) === true) && 571cb0ef41Sopenharmony_ci (newEntries.length === 0 || 581cb0ef41Sopenharmony_ci !newEntries.find((oldEntry) => matchFn(oldEntry, entry)))) { 591cb0ef41Sopenharmony_ci newEntries.unshift(entry) 601cb0ef41Sopenharmony_ci } 611cb0ef41Sopenharmony_ci } 621cb0ef41Sopenharmony_ci 631cb0ef41Sopenharmony_ci const newIndex = '\n' + newEntries.map((entry) => { 641cb0ef41Sopenharmony_ci const stringified = JSON.stringify(entry) 651cb0ef41Sopenharmony_ci const hash = hashEntry(stringified) 661cb0ef41Sopenharmony_ci return `${hash}\t${stringified}` 671cb0ef41Sopenharmony_ci }).join('\n') 681cb0ef41Sopenharmony_ci 691cb0ef41Sopenharmony_ci const setup = async () => { 701cb0ef41Sopenharmony_ci const target = uniqueFilename(path.join(cache, 'tmp'), opts.tmpPrefix) 711cb0ef41Sopenharmony_ci await mkdir(path.dirname(target), { recursive: true }) 721cb0ef41Sopenharmony_ci return { 731cb0ef41Sopenharmony_ci target, 741cb0ef41Sopenharmony_ci moved: false, 751cb0ef41Sopenharmony_ci } 761cb0ef41Sopenharmony_ci } 771cb0ef41Sopenharmony_ci 781cb0ef41Sopenharmony_ci const teardown = async (tmp) => { 791cb0ef41Sopenharmony_ci if (!tmp.moved) { 801cb0ef41Sopenharmony_ci return rm(tmp.target, { recursive: true, force: true }) 811cb0ef41Sopenharmony_ci } 821cb0ef41Sopenharmony_ci } 831cb0ef41Sopenharmony_ci 841cb0ef41Sopenharmony_ci const write = async (tmp) => { 851cb0ef41Sopenharmony_ci await writeFile(tmp.target, newIndex, { flag: 'wx' }) 861cb0ef41Sopenharmony_ci await mkdir(path.dirname(bucket), { recursive: true }) 871cb0ef41Sopenharmony_ci // we use @npmcli/move-file directly here because we 881cb0ef41Sopenharmony_ci // want to overwrite the existing file 891cb0ef41Sopenharmony_ci await moveFile(tmp.target, bucket) 901cb0ef41Sopenharmony_ci tmp.moved = true 911cb0ef41Sopenharmony_ci } 921cb0ef41Sopenharmony_ci 931cb0ef41Sopenharmony_ci // write the file atomically 941cb0ef41Sopenharmony_ci const tmp = await setup() 951cb0ef41Sopenharmony_ci try { 961cb0ef41Sopenharmony_ci await write(tmp) 971cb0ef41Sopenharmony_ci } finally { 981cb0ef41Sopenharmony_ci await teardown(tmp) 991cb0ef41Sopenharmony_ci } 1001cb0ef41Sopenharmony_ci 1011cb0ef41Sopenharmony_ci // we reverse the list we generated such that the newest 1021cb0ef41Sopenharmony_ci // entries come first in order to make looping through them easier 1031cb0ef41Sopenharmony_ci // the true passed to formatEntry tells it to keep null 1041cb0ef41Sopenharmony_ci // integrity values, if they made it this far it's because 1051cb0ef41Sopenharmony_ci // validateEntry returned true, and as such we should return it 1061cb0ef41Sopenharmony_ci return newEntries.reverse().map((entry) => formatEntry(cache, entry, true)) 1071cb0ef41Sopenharmony_ci} 1081cb0ef41Sopenharmony_ci 1091cb0ef41Sopenharmony_cimodule.exports.insert = insert 1101cb0ef41Sopenharmony_ci 1111cb0ef41Sopenharmony_ciasync function insert (cache, key, integrity, opts = {}) { 1121cb0ef41Sopenharmony_ci const { metadata, size, time } = opts 1131cb0ef41Sopenharmony_ci const bucket = bucketPath(cache, key) 1141cb0ef41Sopenharmony_ci const entry = { 1151cb0ef41Sopenharmony_ci key, 1161cb0ef41Sopenharmony_ci integrity: integrity && ssri.stringify(integrity), 1171cb0ef41Sopenharmony_ci time: time || Date.now(), 1181cb0ef41Sopenharmony_ci size, 1191cb0ef41Sopenharmony_ci metadata, 1201cb0ef41Sopenharmony_ci } 1211cb0ef41Sopenharmony_ci try { 1221cb0ef41Sopenharmony_ci await mkdir(path.dirname(bucket), { recursive: true }) 1231cb0ef41Sopenharmony_ci const stringified = JSON.stringify(entry) 1241cb0ef41Sopenharmony_ci // NOTE - Cleverness ahoy! 1251cb0ef41Sopenharmony_ci // 1261cb0ef41Sopenharmony_ci // This works because it's tremendously unlikely for an entry to corrupt 1271cb0ef41Sopenharmony_ci // another while still preserving the string length of the JSON in 1281cb0ef41Sopenharmony_ci // question. So, we just slap the length in there and verify it on read. 1291cb0ef41Sopenharmony_ci // 1301cb0ef41Sopenharmony_ci // Thanks to @isaacs for the whiteboarding session that ended up with 1311cb0ef41Sopenharmony_ci // this. 1321cb0ef41Sopenharmony_ci await appendFile(bucket, `\n${hashEntry(stringified)}\t${stringified}`) 1331cb0ef41Sopenharmony_ci } catch (err) { 1341cb0ef41Sopenharmony_ci if (err.code === 'ENOENT') { 1351cb0ef41Sopenharmony_ci return undefined 1361cb0ef41Sopenharmony_ci } 1371cb0ef41Sopenharmony_ci 1381cb0ef41Sopenharmony_ci throw err 1391cb0ef41Sopenharmony_ci } 1401cb0ef41Sopenharmony_ci return formatEntry(cache, entry) 1411cb0ef41Sopenharmony_ci} 1421cb0ef41Sopenharmony_ci 1431cb0ef41Sopenharmony_cimodule.exports.find = find 1441cb0ef41Sopenharmony_ci 1451cb0ef41Sopenharmony_ciasync function find (cache, key) { 1461cb0ef41Sopenharmony_ci const bucket = bucketPath(cache, key) 1471cb0ef41Sopenharmony_ci try { 1481cb0ef41Sopenharmony_ci const entries = await bucketEntries(bucket) 1491cb0ef41Sopenharmony_ci return entries.reduce((latest, next) => { 1501cb0ef41Sopenharmony_ci if (next && next.key === key) { 1511cb0ef41Sopenharmony_ci return formatEntry(cache, next) 1521cb0ef41Sopenharmony_ci } else { 1531cb0ef41Sopenharmony_ci return latest 1541cb0ef41Sopenharmony_ci } 1551cb0ef41Sopenharmony_ci }, null) 1561cb0ef41Sopenharmony_ci } catch (err) { 1571cb0ef41Sopenharmony_ci if (err.code === 'ENOENT') { 1581cb0ef41Sopenharmony_ci return null 1591cb0ef41Sopenharmony_ci } else { 1601cb0ef41Sopenharmony_ci throw err 1611cb0ef41Sopenharmony_ci } 1621cb0ef41Sopenharmony_ci } 1631cb0ef41Sopenharmony_ci} 1641cb0ef41Sopenharmony_ci 1651cb0ef41Sopenharmony_cimodule.exports.delete = del 1661cb0ef41Sopenharmony_ci 1671cb0ef41Sopenharmony_cifunction del (cache, key, opts = {}) { 1681cb0ef41Sopenharmony_ci if (!opts.removeFully) { 1691cb0ef41Sopenharmony_ci return insert(cache, key, null, opts) 1701cb0ef41Sopenharmony_ci } 1711cb0ef41Sopenharmony_ci 1721cb0ef41Sopenharmony_ci const bucket = bucketPath(cache, key) 1731cb0ef41Sopenharmony_ci return rm(bucket, { recursive: true, force: true }) 1741cb0ef41Sopenharmony_ci} 1751cb0ef41Sopenharmony_ci 1761cb0ef41Sopenharmony_cimodule.exports.lsStream = lsStream 1771cb0ef41Sopenharmony_ci 1781cb0ef41Sopenharmony_cifunction lsStream (cache) { 1791cb0ef41Sopenharmony_ci const indexDir = bucketDir(cache) 1801cb0ef41Sopenharmony_ci const stream = new Minipass({ objectMode: true }) 1811cb0ef41Sopenharmony_ci 1821cb0ef41Sopenharmony_ci // Set all this up to run on the stream and then just return the stream 1831cb0ef41Sopenharmony_ci Promise.resolve().then(async () => { 1841cb0ef41Sopenharmony_ci const buckets = await readdirOrEmpty(indexDir) 1851cb0ef41Sopenharmony_ci await Promise.all(buckets.map(async (bucket) => { 1861cb0ef41Sopenharmony_ci const bucketPath = path.join(indexDir, bucket) 1871cb0ef41Sopenharmony_ci const subbuckets = await readdirOrEmpty(bucketPath) 1881cb0ef41Sopenharmony_ci await Promise.all(subbuckets.map(async (subbucket) => { 1891cb0ef41Sopenharmony_ci const subbucketPath = path.join(bucketPath, subbucket) 1901cb0ef41Sopenharmony_ci 1911cb0ef41Sopenharmony_ci // "/cachename/<bucket 0xFF>/<bucket 0xFF>./*" 1921cb0ef41Sopenharmony_ci const subbucketEntries = await readdirOrEmpty(subbucketPath) 1931cb0ef41Sopenharmony_ci await Promise.all(subbucketEntries.map(async (entry) => { 1941cb0ef41Sopenharmony_ci const entryPath = path.join(subbucketPath, entry) 1951cb0ef41Sopenharmony_ci try { 1961cb0ef41Sopenharmony_ci const entries = await bucketEntries(entryPath) 1971cb0ef41Sopenharmony_ci // using a Map here prevents duplicate keys from showing up 1981cb0ef41Sopenharmony_ci // twice, I guess? 1991cb0ef41Sopenharmony_ci const reduced = entries.reduce((acc, entry) => { 2001cb0ef41Sopenharmony_ci acc.set(entry.key, entry) 2011cb0ef41Sopenharmony_ci return acc 2021cb0ef41Sopenharmony_ci }, new Map()) 2031cb0ef41Sopenharmony_ci // reduced is a map of key => entry 2041cb0ef41Sopenharmony_ci for (const entry of reduced.values()) { 2051cb0ef41Sopenharmony_ci const formatted = formatEntry(cache, entry) 2061cb0ef41Sopenharmony_ci if (formatted) { 2071cb0ef41Sopenharmony_ci stream.write(formatted) 2081cb0ef41Sopenharmony_ci } 2091cb0ef41Sopenharmony_ci } 2101cb0ef41Sopenharmony_ci } catch (err) { 2111cb0ef41Sopenharmony_ci if (err.code === 'ENOENT') { 2121cb0ef41Sopenharmony_ci return undefined 2131cb0ef41Sopenharmony_ci } 2141cb0ef41Sopenharmony_ci throw err 2151cb0ef41Sopenharmony_ci } 2161cb0ef41Sopenharmony_ci })) 2171cb0ef41Sopenharmony_ci })) 2181cb0ef41Sopenharmony_ci })) 2191cb0ef41Sopenharmony_ci stream.end() 2201cb0ef41Sopenharmony_ci return stream 2211cb0ef41Sopenharmony_ci }).catch(err => stream.emit('error', err)) 2221cb0ef41Sopenharmony_ci 2231cb0ef41Sopenharmony_ci return stream 2241cb0ef41Sopenharmony_ci} 2251cb0ef41Sopenharmony_ci 2261cb0ef41Sopenharmony_cimodule.exports.ls = ls 2271cb0ef41Sopenharmony_ci 2281cb0ef41Sopenharmony_ciasync function ls (cache) { 2291cb0ef41Sopenharmony_ci const entries = await lsStream(cache).collect() 2301cb0ef41Sopenharmony_ci return entries.reduce((acc, xs) => { 2311cb0ef41Sopenharmony_ci acc[xs.key] = xs 2321cb0ef41Sopenharmony_ci return acc 2331cb0ef41Sopenharmony_ci }, {}) 2341cb0ef41Sopenharmony_ci} 2351cb0ef41Sopenharmony_ci 2361cb0ef41Sopenharmony_cimodule.exports.bucketEntries = bucketEntries 2371cb0ef41Sopenharmony_ci 2381cb0ef41Sopenharmony_ciasync function bucketEntries (bucket, filter) { 2391cb0ef41Sopenharmony_ci const data = await readFile(bucket, 'utf8') 2401cb0ef41Sopenharmony_ci return _bucketEntries(data, filter) 2411cb0ef41Sopenharmony_ci} 2421cb0ef41Sopenharmony_ci 2431cb0ef41Sopenharmony_cifunction _bucketEntries (data, filter) { 2441cb0ef41Sopenharmony_ci const entries = [] 2451cb0ef41Sopenharmony_ci data.split('\n').forEach((entry) => { 2461cb0ef41Sopenharmony_ci if (!entry) { 2471cb0ef41Sopenharmony_ci return 2481cb0ef41Sopenharmony_ci } 2491cb0ef41Sopenharmony_ci 2501cb0ef41Sopenharmony_ci const pieces = entry.split('\t') 2511cb0ef41Sopenharmony_ci if (!pieces[1] || hashEntry(pieces[1]) !== pieces[0]) { 2521cb0ef41Sopenharmony_ci // Hash is no good! Corruption or malice? Doesn't matter! 2531cb0ef41Sopenharmony_ci // EJECT EJECT 2541cb0ef41Sopenharmony_ci return 2551cb0ef41Sopenharmony_ci } 2561cb0ef41Sopenharmony_ci let obj 2571cb0ef41Sopenharmony_ci try { 2581cb0ef41Sopenharmony_ci obj = JSON.parse(pieces[1]) 2591cb0ef41Sopenharmony_ci } catch (_) { 2601cb0ef41Sopenharmony_ci // eslint-ignore-next-line no-empty-block 2611cb0ef41Sopenharmony_ci } 2621cb0ef41Sopenharmony_ci // coverage disabled here, no need to test with an entry that parses to something falsey 2631cb0ef41Sopenharmony_ci // istanbul ignore else 2641cb0ef41Sopenharmony_ci if (obj) { 2651cb0ef41Sopenharmony_ci entries.push(obj) 2661cb0ef41Sopenharmony_ci } 2671cb0ef41Sopenharmony_ci }) 2681cb0ef41Sopenharmony_ci return entries 2691cb0ef41Sopenharmony_ci} 2701cb0ef41Sopenharmony_ci 2711cb0ef41Sopenharmony_cimodule.exports.bucketDir = bucketDir 2721cb0ef41Sopenharmony_ci 2731cb0ef41Sopenharmony_cifunction bucketDir (cache) { 2741cb0ef41Sopenharmony_ci return path.join(cache, `index-v${indexV}`) 2751cb0ef41Sopenharmony_ci} 2761cb0ef41Sopenharmony_ci 2771cb0ef41Sopenharmony_cimodule.exports.bucketPath = bucketPath 2781cb0ef41Sopenharmony_ci 2791cb0ef41Sopenharmony_cifunction bucketPath (cache, key) { 2801cb0ef41Sopenharmony_ci const hashed = hashKey(key) 2811cb0ef41Sopenharmony_ci return path.join.apply( 2821cb0ef41Sopenharmony_ci path, 2831cb0ef41Sopenharmony_ci [bucketDir(cache)].concat(hashToSegments(hashed)) 2841cb0ef41Sopenharmony_ci ) 2851cb0ef41Sopenharmony_ci} 2861cb0ef41Sopenharmony_ci 2871cb0ef41Sopenharmony_cimodule.exports.hashKey = hashKey 2881cb0ef41Sopenharmony_ci 2891cb0ef41Sopenharmony_cifunction hashKey (key) { 2901cb0ef41Sopenharmony_ci return hash(key, 'sha256') 2911cb0ef41Sopenharmony_ci} 2921cb0ef41Sopenharmony_ci 2931cb0ef41Sopenharmony_cimodule.exports.hashEntry = hashEntry 2941cb0ef41Sopenharmony_ci 2951cb0ef41Sopenharmony_cifunction hashEntry (str) { 2961cb0ef41Sopenharmony_ci return hash(str, 'sha1') 2971cb0ef41Sopenharmony_ci} 2981cb0ef41Sopenharmony_ci 2991cb0ef41Sopenharmony_cifunction hash (str, digest) { 3001cb0ef41Sopenharmony_ci return crypto 3011cb0ef41Sopenharmony_ci .createHash(digest) 3021cb0ef41Sopenharmony_ci .update(str) 3031cb0ef41Sopenharmony_ci .digest('hex') 3041cb0ef41Sopenharmony_ci} 3051cb0ef41Sopenharmony_ci 3061cb0ef41Sopenharmony_cifunction formatEntry (cache, entry, keepAll) { 3071cb0ef41Sopenharmony_ci // Treat null digests as deletions. They'll shadow any previous entries. 3081cb0ef41Sopenharmony_ci if (!entry.integrity && !keepAll) { 3091cb0ef41Sopenharmony_ci return null 3101cb0ef41Sopenharmony_ci } 3111cb0ef41Sopenharmony_ci 3121cb0ef41Sopenharmony_ci return { 3131cb0ef41Sopenharmony_ci key: entry.key, 3141cb0ef41Sopenharmony_ci integrity: entry.integrity, 3151cb0ef41Sopenharmony_ci path: entry.integrity ? contentPath(cache, entry.integrity) : undefined, 3161cb0ef41Sopenharmony_ci size: entry.size, 3171cb0ef41Sopenharmony_ci time: entry.time, 3181cb0ef41Sopenharmony_ci metadata: entry.metadata, 3191cb0ef41Sopenharmony_ci } 3201cb0ef41Sopenharmony_ci} 3211cb0ef41Sopenharmony_ci 3221cb0ef41Sopenharmony_cifunction readdirOrEmpty (dir) { 3231cb0ef41Sopenharmony_ci return readdir(dir).catch((err) => { 3241cb0ef41Sopenharmony_ci if (err.code === 'ENOENT' || err.code === 'ENOTDIR') { 3251cb0ef41Sopenharmony_ci return [] 3261cb0ef41Sopenharmony_ci } 3271cb0ef41Sopenharmony_ci 3281cb0ef41Sopenharmony_ci throw err 3291cb0ef41Sopenharmony_ci }) 3301cb0ef41Sopenharmony_ci} 331