Skip to content

Commit

Permalink
feat(cache): initial implementation of ls and rm
Browse files Browse the repository at this point in the history
PR-URL: #3592
Credit: @fritzy
Close: #3592
Reviewed-by: @nlf
  • Loading branch information
fritzy authored and wraithgar committed Aug 18, 2021
1 parent 8183976 commit ff34d6c
Show file tree
Hide file tree
Showing 4 changed files with 382 additions and 31 deletions.
128 changes: 107 additions & 21 deletions lib/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,59 @@ const log = require('npmlog')
const pacote = require('pacote')
const path = require('path')
const rimraf = promisify(require('rimraf'))
const semver = require('semver')
const BaseCommand = require('./base-command.js')
const npa = require('npm-package-arg')
const jsonParse = require('json-parse-even-better-errors')

const searchCachePackage = async (path, spec, cacheKeys) => {
const parsed = npa(spec)
if (parsed.rawSpec !== '' && parsed.type === 'tag')
throw new Error(`Cannot list cache keys for a tagged package.`)
const searchMFH = new RegExp(`^make-fetch-happen:request-cache:.*(?<!/[@a-zA-Z]+)/${parsed.name}/-/(${parsed.name}[^/]+.tgz)$`)
const searchPack = new RegExp(`^make-fetch-happen:request-cache:.*/${parsed.escapedName}$`)
const results = new Set()
cacheKeys = new Set(cacheKeys)
for (const key of cacheKeys) {
// match on the public key registry url format
if (searchMFH.test(key)) {
// extract the version from the filename
const filename = key.match(searchMFH)[1]
const noExt = filename.slice(0, -4)
const noScope = `${parsed.name.split('/').pop()}-`
const ver = noExt.slice(noScope.length)
if (semver.satisfies(ver, parsed.rawSpec))
results.add(key)
continue
}
// is this key a packument?
if (!searchPack.test(key))
continue

results.add(key)
let packument, details
try {
details = await cacache.get(path, key)
packument = jsonParse(details.data)
} catch (_) {
// if we couldn't parse the packument, abort
continue
}
if (!packument.versions || typeof packument.versions !== 'object')
continue
// assuming this is a packument
for (const ver of Object.keys(packument.versions)) {
if (semver.satisfies(ver, parsed.rawSpec)) {
if (packument.versions[ver].dist
&& typeof packument.versions[ver].dist === 'object'
&& packument.versions[ver].dist.tarball !== undefined
&& cacheKeys.has(`make-fetch-happen:request-cache:${packument.versions[ver].dist.tarball}`))
results.add(`make-fetch-happen:request-cache:${packument.versions[ver].dist.tarball}`)
}
}
}
return results
}

class Cache extends BaseCommand {
static get description () {
Expand All @@ -29,21 +81,24 @@ class Cache extends BaseCommand {
'add <tarball url>',
'add <git url>',
'add <name>@<version>',
'clean',
'clean [<key>]',
'ls [<name>@<version>]',
'verify',
]
}

async completion (opts) {
const argv = opts.conf.argv.remain
if (argv.length === 2)
return ['add', 'clean', 'verify']
return ['add', 'clean', 'verify', 'ls', 'delete']

// TODO - eventually...
switch (argv[2]) {
case 'verify':
case 'clean':
case 'add':
case 'ls':
case 'delete':
return []
}
}
Expand All @@ -61,34 +116,47 @@ class Cache extends BaseCommand {
return await this.add(args)
case 'verify': case 'check':
return await this.verify()
case 'ls':
return await this.ls(args)
default:
throw Object.assign(new Error(this.usage), { code: 'EUSAGE' })
}
}

// npm cache clean [pkg]*
async clean (args) {
if (args.length)
throw new Error('npm cache clear does not accept arguments')

const cachePath = path.join(this.npm.cache, '_cacache')
if (!this.npm.config.get('force')) {
throw new Error(`As of npm@5, the npm cache self-heals from corruption issues
by treating integrity mismatches as cache misses. As a result,
data extracted from the cache is guaranteed to be valid. If you
want to make sure everything is consistent, use \`npm cache verify\`
instead. Deleting the cache can only make npm go slower, and is
not likely to correct any problems you may be encountering!
On the other hand, if you're debugging an issue with the installer,
or race conditions that depend on the timing of writing to an empty
cache, you can use \`npm install --cache /tmp/empty-cache\` to use a
temporary cache instead of nuking the actual one.
If you're sure you want to delete the entire cache, rerun this command
with --force.`)
if (args.length === 0) {
if (!this.npm.config.get('force')) {
throw new Error(`As of npm@5, the npm cache self-heals from corruption issues
by treating integrity mismatches as cache misses. As a result,
data extracted from the cache is guaranteed to be valid. If you
want to make sure everything is consistent, use \`npm cache verify\`
instead. Deleting the cache can only make npm go slower, and is
not likely to correct any problems you may be encountering!
On the other hand, if you're debugging an issue with the installer,
or race conditions that depend on the timing of writing to an empty
cache, you can use \`npm install --cache /tmp/empty-cache\` to use a
temporary cache instead of nuking the actual one.
If you're sure you want to delete the entire cache, rerun this command
with --force.`)
}
return rimraf(cachePath)
}
for (const key of args) {
let entry
try {
entry = await cacache.get(cachePath, key)
} catch (err) {
this.npm.log.warn(`Not Found: ${key}`)
break
}
this.npm.output(`Deleted: ${key}`)
await cacache.rm.entry(cachePath, key)
await cacache.rm.content(cachePath, entry.integrity)
}
return rimraf(cachePath)
}

// npm cache add <tarball-url>...
Expand Down Expand Up @@ -131,6 +199,24 @@ with --force.`)
this.npm.output(`Index entries: ${stats.totalEntries}`)
this.npm.output(`Finished in ${stats.runTime.total / 1000}s`)
}

// npm cache ls [--package <spec> ...]
async ls (specs) {
const cachePath = path.join(this.npm.cache, '_cacache')
const cacheKeys = Object.keys(await cacache.ls(cachePath))
if (specs.length > 0) {
// get results for each package spec specified
const results = new Set()
for (const spec of specs) {
const keySet = await searchCachePackage(cachePath, spec, cacheKeys)
for (const key of keySet)
results.add(key)
}
[...results].sort((a, b) => a.localeCompare(b, 'en')).forEach(key => this.npm.output(key))
return
}
cacheKeys.sort((a, b) => a.localeCompare(b, 'en')).forEach(key => this.npm.output(key))
}
}

module.exports = Cache
3 changes: 2 additions & 1 deletion tap-snapshots/test/lib/load-all-commands.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ npm cache add <folder>
npm cache add <tarball url>
npm cache add <git url>
npm cache add <name>@<version>
npm cache clean
npm cache clean [<key>]
npm cache ls [<name>@<version>]
npm cache verify
Options:
Expand Down
3 changes: 2 additions & 1 deletion tap-snapshots/test/lib/utils/npm-usage.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,8 @@ All commands:
npm cache add <tarball url>
npm cache add <git url>
npm cache add <name>@<version>
npm cache clean
npm cache clean [<key>]
npm cache ls [<name>@<version>]
npm cache verify
Options:
Expand Down
Loading

0 comments on commit ff34d6c

Please sign in to comment.