disaster-restore вернул тарболлы но древний/пустой packument →
publish свежей версии EEXISTS 409. Fix: снять только коллизирующий
целевой .tgz (backup first), republish; НЕ rm -rf каталог.
Применено к @snollajs/{data,mailer,numbering,content-api}.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
78 lines
4.6 KiB
Markdown
78 lines
4.6 KiB
Markdown
---
|
||
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 <pkg>@<newver>` падает с
|
||
**`409 "this package is already present"`**, хотя в реестре этой версии (по GET) нет —
|
||
`dist-tags.latest` показывает древнюю версию. На некоторых пакетах GET вообще `404`.
|
||
|
||
## Root cause: packument ↔ tarball рассинхрон
|
||
|
||
Restore вернул в `/storage/.../<pkg>/`:
|
||
- **все `.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/.../<pkg>` снимет 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 <pkg-dirs>
|
||
# снять коллизию — ровно целевой тарболл, остальное не трогать
|
||
sudo rm /opt/stacks/verdaccio/storage/@scope/<pkg>/<pkg>-<targetver>.tgz
|
||
# → republish <pkg>@<targetver> из 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]] — хост
|