Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add cache commands "ls" and "rm" #3592

Merged
merged 1 commit into from Aug 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
128 changes: 107 additions & 21 deletions lib/cache.js
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'
fritzy marked this conversation as resolved.
Show resolved Hide resolved
&& 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
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
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