--- title: Verdaccio disaster-restore desync — tarball present, packument stale → EEXISTS 409 type: concept tags: [verdaccio, npm, storage, restore, backup, 409, gotcha, postmortem] sources: [] updated: 2026-06-11 --- # Verdaccio disaster-restore desync — 409 при publish свежей версии ## Симптом После disaster-restore verdaccio из бэкапа: `npm/yarn publish @` падает с **`409 "this package is already present"`**, хотя в реестре этой версии (по GET) нет — `dist-tags.latest` показывает древнюю версию. На некоторых пакетах GET вообще `404`. ## Root cause: packument ↔ tarball рассинхрон Restore вернул в `/storage/...//`: - **все `.tgz` тарболлы** — до текущих версий включительно (`data-0.9.0.tgz` физически на диске); - но **`package.json` (packument-индекс) — древний** (`versions{}` знает только 0.1.x/0.2.x, `dist-tags.latest=0.2.0`), потому что метадата восстановилась из старого слоя бэкапа либо была затёрта сорванным `unpublish`. Verdaccio publish-flow: 1. merge новой версии в packument → **OK** (версии нет в индексе); 2. запись `.tgz` на диск → файл **уже существует** (с restore) → `EEXISTS` → surface как **`409 this package is already present`**. Лог-отпечаток: ``` PUT /@snollajs%2Fdata http <-- 409 ... error: this package is already present ``` ### Подвид: пустой packument после сорванного unpublish `unpublish` пишет пустой packument-stub (161 байт, `versions:[]`, `_rev:""`), затем пытается `rmdir` каталог → **`ENOTEMPTY`** (тарболлы на месте) → `500`. Итог: GET `404` (пустой индекс), publish `409` (тарболл цел). Каталог «полупустой»: индекс мёртв, бинарники живы. ## Fix: хирургия, НЕ `rm -rf` каталога `rm -rf /storage/.../` снимет 409, но **уничтожит всю историю версий** (тарболлы — locally-published, `_distfiles:{}` ⇒ единственная копия, см. [[verdaccio-prune-semantics]]). Правильно — удалить **только коллизирующий целевой `.tgz`** под версию, которую republish'ат: ```bash # backup сначала sudo tar czf /tmp/verdaccio-rescue-$(date -u +%Y%m%dT%H%M%SZ).tar.gz # снять коллизию — ровно целевой тарболл, остальное не трогать sudo rm /opt/stacks/verdaccio/storage/@scope//-.tgz # → republish @ из source проходит, packument регенерится корректно ``` Рестарт контейнера **не нужен**: local-storage читает packument/тарболл с диска пер-реквест; packument не трогаем, только убираем лишний `.tgz`, которого в индексе и так нет. **Trade-off:** промежуточные версии (тарболлы на диске, но не в индексе после republish) остаются orphaned — безвредны, не ставятся по pin, GC их не трогает (locally-published). Если нужна полная история в индексе — отдельный скрипт-reconstruct packument из всех on-disk тарболлов (`tar xzO package/package.json` per tgz + shasum/integrity) — heavier, по умолчанию не делаем. ## Применено 2026-06-11 `@snollajs/{data@0.9.0, mailer@0.7.4, numbering@0.7.4, content-api@0.8.1}` — удалены 4 коллизирующих тарболла, бэкап `/tmp/verdaccio-snollajs-rescue-20260611T140801Z.tar.gz` на [[../entities/vds-kzntsv]]. Republish делегирован владельцу пакетов (сессия snolla). Контекст: восстановление verdaccio после wipe (см. также [[verdaccio-token-lifecycle]] — другой класс отказа того же restore-эпизода). ## Связанные - [[verdaccio-prune-semantics]] — locally-published vs proxied, почему `rm` тарболла = permanent loss - [[verdaccio-token-lifecycle]] — restart trap / ephemeral secret (соседний симптом того же restore) - [[../entities/vds-kzntsv]] — хост