--- title: Verdaccio prune semantics — proxied vs locally-published type: concept tags: [verdaccio, npm, storage, gc, gotcha] sources: [] updated: 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.json` — packument (metadata, дешёвый) - `-.tgz` — binary tarballs (дорогие) Для scoped: `/storage/data///...`. Scope в filename `.tgz` **НЕ** включается — `@snollajs/snolla/snolla-0.2.4.tgz`. ## Detection rule В `package.json` есть три relevant fields: ```yaml _uplinks: { npmjs: { etag, fetched } } # upstream registries verdaccio queried _distfiles: { "-.tgz": { url, sha, registry } } # ключи — ИМЯ tarball, не version _attachments: { "-.tgz": { shasum } } # cached + locally-uploaded tarballs ``` **Detection**: - `_distfiles[]` defined → **proxied**, tarball mirrored from upstream URL. Удалять `.tgz` файл безопасно — verdaccio re-fetch'нет при следующем `npm install pkg@v`. - `_distfiles[]` undefined, but `_attachments[]` 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[-.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 = "-.tgz" if _distfiles[tgzName] undefined: continue # locally-published, skip if .tgz file exists: delete file if _attachments[tgzName] exists: delete entry если что-то изменили в _attachments: bump _rev (`-`) 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 в формате `-` (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`](../../.tasks/vds-gc-cron.md) — 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). ## Ссылки - Verdaccio local-storage docs: https://verdaccio.org/docs/configuration#storage - Algorithm в репо: `/opt/stacks/gc/scripts/verdaccio-prune.js` на VDS.