Files
admin/.wiki/concepts/verdaccio-prune-semantics.md

4.4 KiB
Raw Blame History

title, type, tags, sources, updated
title type tags sources updated
Verdaccio prune semantics — proxied vs locally-published concept
verdaccio
npm
storage
gc
gotcha
2026-05-21

Verdaccio prune — proxied vs locally-published

Прежде чем удалять .tgz из verdaccio storage, надо отличать proxied (cached from upstream — безопасно удалить, можно re-fetch) от locally-published (authoritative copy — удаление = permanent loss).

Storage layout

/storage/data/<package>/:

  • package.json — packument (metadata, дешёвый)
  • <package>-<version>.tgz — binary tarballs (дорогие)

Для scoped: /storage/data/<scope>/<package>/.... Scope в filename .tgz НЕ включается — @snollajs/snolla/snolla-0.2.4.tgz.

Detection rule

В package.json есть три relevant fields:

_uplinks: { npmjs: { etag, fetched } }      # upstream registries verdaccio queried
_distfiles: { "<pkg>-<v>.tgz": { url, sha, registry } }   # ключи — ИМЯ tarball, не version
_attachments: { "<pkg>-<v>.tgz": { shasum } }             # cached + locally-uploaded tarballs

Detection:

  • _distfiles[<tgzName>] defined → proxied, tarball mirrored from upstream URL. Удалять .tgz файл безопасно — verdaccio re-fetch'нет при следующем npm install pkg@v.
  • _distfiles[<tgzName>] undefined, but _attachments[<tgzName>] defined и .tgz есть на диске → locally-published (npm publish в verdaccio через verdaccio registry). НЕ удалять — это единственная копия.

Gotcha: keying

Ключи _distfiles — это filename (lodash-0.1.0.tgz), НЕ version string (0.1.0). Lookup _distfiles["0.1.0"] всегда возвращает undefined, ложно классифицируя ВСЕ versions как locally-published.

Easy mistake: первый draft prune скрипта проверял _distfiles[v] → dry-run показал 4597/7021 packages "защищены" (на самом деле ВСЕ proxied lodash/react/etc). Fix: _distfiles[<tgzBase>-<v>.tgz] где tgzBase = name.replace(/^@[^/]+\//, '') (strip scope для filename).

Safe prune algorithm

для каждого package:
  keepSet = dist-tagged versions  # {latest, next, beta, ...}
  others = versions  keepSet, отсортированные по time[v] desc
  keepSet += first (N - |keepSet|) из others   # N = 10
  для v в (versions  keepSet):
    tgzName = "<scope-stripped-name>-<v>.tgz"
    if _distfiles[tgzName] undefined:
      continue   # locally-published, skip
    if .tgz file exists: delete file
    if _attachments[tgzName] exists: delete entry
  если что-то изменили в _attachments:
    bump _rev (`<num+1>-<random_hex>`)
    atomic rewrite package.json (tmp + rename)

Atomic write + _rev

package.json rewrite через fs.writeFileSync(tmp); fs.renameSync(tmp, real) — atomic on POSIX same-fs. Race window между «file gone» и «package.json updated» ничтожен.

_rev field в формате <num>-<hash> (CouchDB-style). Verdaccio проверяет его для optimistic concurrency. Bump = increment number + new random hex. Это invalidates npm client packument cache.

Дополнительная подстраховка от race: stop verdaccio перед prune, start после (~1 min downtime weekly). Альтернатива — atomic rewrite без stop — допустима, но cache invalidation менее clean.

Что НЕ трогать

  • versions{} и time{} — оставляем metadata in-place даже после удаления tarballs. Для proxied packages npm re-fetch'нет tgz при request; metadata лёгкая (~KB).
  • _uplinks{} — agent-level state о fetch timestamps; не относится к individual versions.
  • readme, users, _id — irrelevant для prune.

Применено

vds-gc-cron — weekly cron Sun 03:30 MSK на VDS. Smoke 2026-05-21: 8.5G→6.8G freed, 4524 tgz deleted, 73 locally-published versions защищены (@snollajs/* + ~40 другие internal packages).

Ссылки