1const cacache = require('cacache') 2const pacote = require('pacote') 3const fs = require('fs/promises') 4const { join } = require('path') 5const semver = require('semver') 6const BaseCommand = require('../base-command.js') 7const npa = require('npm-package-arg') 8const jsonParse = require('json-parse-even-better-errors') 9const localeCompare = require('@isaacs/string-locale-compare')('en') 10const log = require('../utils/log-shim') 11 12const searchCachePackage = async (path, parsed, cacheKeys) => { 13 /* eslint-disable-next-line max-len */ 14 const searchMFH = new RegExp(`^make-fetch-happen:request-cache:.*(?<!/[@a-zA-Z]+)/${parsed.name}/-/(${parsed.name}[^/]+.tgz)$`) 15 const searchPack = new RegExp(`^make-fetch-happen:request-cache:.*/${parsed.escapedName}$`) 16 const results = new Set() 17 cacheKeys = new Set(cacheKeys) 18 for (const key of cacheKeys) { 19 // match on the public key registry url format 20 if (searchMFH.test(key)) { 21 // extract the version from the filename 22 const filename = key.match(searchMFH)[1] 23 const noExt = filename.slice(0, -4) 24 const noScope = `${parsed.name.split('/').pop()}-` 25 const ver = noExt.slice(noScope.length) 26 if (semver.satisfies(ver, parsed.rawSpec)) { 27 results.add(key) 28 } 29 continue 30 } 31 // is this key a packument? 32 if (!searchPack.test(key)) { 33 continue 34 } 35 36 results.add(key) 37 let packument, details 38 try { 39 details = await cacache.get(path, key) 40 packument = jsonParse(details.data) 41 } catch (_) { 42 // if we couldn't parse the packument, abort 43 continue 44 } 45 if (!packument.versions || typeof packument.versions !== 'object') { 46 continue 47 } 48 49 // assuming this is a packument 50 for (const ver of Object.keys(packument.versions)) { 51 if (semver.satisfies(ver, parsed.rawSpec)) { 52 if (packument.versions[ver].dist && 53 typeof packument.versions[ver].dist === 'object' && 54 packument.versions[ver].dist.tarball !== undefined && 55 cacheKeys.has(`make-fetch-happen:request-cache:${packument.versions[ver].dist.tarball}`) 56 ) { 57 results.add(`make-fetch-happen:request-cache:${packument.versions[ver].dist.tarball}`) 58 } 59 } 60 } 61 } 62 return results 63} 64 65class Cache extends BaseCommand { 66 static description = 'Manipulates packages cache' 67 static name = 'cache' 68 static params = ['cache'] 69 static usage = [ 70 'add <package-spec>', 71 'clean [<key>]', 72 'ls [<name>@<version>]', 73 'verify', 74 ] 75 76 static async completion (opts) { 77 const argv = opts.conf.argv.remain 78 if (argv.length === 2) { 79 return ['add', 'clean', 'verify', 'ls'] 80 } 81 82 // TODO - eventually... 83 switch (argv[2]) { 84 case 'verify': 85 case 'clean': 86 case 'add': 87 case 'ls': 88 return [] 89 } 90 } 91 92 async exec (args) { 93 const cmd = args.shift() 94 switch (cmd) { 95 case 'rm': case 'clear': case 'clean': 96 return await this.clean(args) 97 case 'add': 98 return await this.add(args) 99 case 'verify': case 'check': 100 return await this.verify() 101 case 'ls': 102 return await this.ls(args) 103 default: 104 throw this.usageError() 105 } 106 } 107 108 // npm cache clean [pkg]* 109 async clean (args) { 110 const cachePath = join(this.npm.cache, '_cacache') 111 if (args.length === 0) { 112 if (!this.npm.config.get('force')) { 113 throw new Error(`As of npm@5, the npm cache self-heals from corruption issues 114 by treating integrity mismatches as cache misses. As a result, 115 data extracted from the cache is guaranteed to be valid. If you 116 want to make sure everything is consistent, use \`npm cache verify\` 117 instead. Deleting the cache can only make npm go slower, and is 118 not likely to correct any problems you may be encountering! 119 120 On the other hand, if you're debugging an issue with the installer, 121 or race conditions that depend on the timing of writing to an empty 122 cache, you can use \`npm install --cache /tmp/empty-cache\` to use a 123 temporary cache instead of nuking the actual one. 124 125 If you're sure you want to delete the entire cache, rerun this command 126 with --force.`) 127 } 128 return fs.rm(cachePath, { recursive: true, force: true }) 129 } 130 for (const key of args) { 131 let entry 132 try { 133 entry = await cacache.get(cachePath, key) 134 } catch (err) { 135 log.warn(`Not Found: ${key}`) 136 break 137 } 138 this.npm.output(`Deleted: ${key}`) 139 await cacache.rm.entry(cachePath, key) 140 // XXX this could leave other entries without content! 141 await cacache.rm.content(cachePath, entry.integrity) 142 } 143 } 144 145 // npm cache add <tarball-url>... 146 // npm cache add <pkg> <ver>... 147 // npm cache add <tarball>... 148 // npm cache add <folder>... 149 async add (args) { 150 log.silly('cache add', 'args', args) 151 if (args.length === 0) { 152 throw this.usageError('First argument to `add` is required') 153 } 154 155 return Promise.all(args.map(spec => { 156 log.silly('cache add', 'spec', spec) 157 // we ask pacote for the thing, and then just throw the data 158 // away so that it tee-pipes it into the cache like it does 159 // for a normal request. 160 return pacote.tarball.stream(spec, stream => { 161 stream.resume() 162 return stream.promise() 163 }, { ...this.npm.flatOptions }) 164 })) 165 } 166 167 async verify () { 168 const cache = join(this.npm.cache, '_cacache') 169 const prefix = cache.indexOf(process.env.HOME) === 0 170 ? `~${cache.slice(process.env.HOME.length)}` 171 : cache 172 const stats = await cacache.verify(cache) 173 this.npm.output(`Cache verified and compressed (${prefix})`) 174 this.npm.output(`Content verified: ${stats.verifiedContent} (${stats.keptSize} bytes)`) 175 if (stats.badContentCount) { 176 this.npm.output(`Corrupted content removed: ${stats.badContentCount}`) 177 } 178 if (stats.reclaimedCount) { 179 /* eslint-disable-next-line max-len */ 180 this.npm.output(`Content garbage-collected: ${stats.reclaimedCount} (${stats.reclaimedSize} bytes)`) 181 } 182 if (stats.missingContent) { 183 this.npm.output(`Missing content: ${stats.missingContent}`) 184 } 185 this.npm.output(`Index entries: ${stats.totalEntries}`) 186 this.npm.output(`Finished in ${stats.runTime.total / 1000}s`) 187 } 188 189 // npm cache ls [--package <spec> ...] 190 async ls (specs) { 191 const cachePath = join(this.npm.cache, '_cacache') 192 const cacheKeys = Object.keys(await cacache.ls(cachePath)) 193 if (specs.length > 0) { 194 // get results for each package spec specified 195 const results = new Set() 196 for (const spec of specs) { 197 const parsed = npa(spec) 198 if (parsed.rawSpec !== '' && parsed.type === 'tag') { 199 throw this.usageError('Cannot list cache keys for a tagged package.') 200 } 201 const keySet = await searchCachePackage(cachePath, parsed, cacheKeys) 202 for (const key of keySet) { 203 results.add(key) 204 } 205 } 206 [...results].sort(localeCompare).forEach(key => this.npm.output(key)) 207 return 208 } 209 cacheKeys.sort(localeCompare).forEach(key => this.npm.output(key)) 210 } 211} 212 213module.exports = Cache 214