Идентичность на ULID: download_infohash, guarded-дедуп, миграция (ulid-identity)
Все сущности переехали с INTEGER AUTOINCREMENT на TEXT ULID (lowercase, internal/ident — единая точка генерации и разбора; oklog/ulid). Инфохэши загрузки — множество (download_infohash, v1/v2 гибридных торрентов): дедуп и сопоставление в поллинге по любому из хешей, magnet-парсер отдаёт оба хеша гибридной ссылки, усечённый v2-хеш v2-only раздач не хранится. Инвариант «не более одной активной загрузки на infohash» вместо снятого unique-индекса держат guarded-методы store в одной write-транзакции (_txlock=immediate): CreateDownloadIfNoActive (приём/adopt, с доносом недостающих хешей), ActivateIfNoOtherActive (retry/recovery/relink, отказ до побочных эффектов), guarded AddInfohashes; SetDownloadState отклоняет терминал→активное как механический бэкстоп. Миграция 0006 — первая Go-миграция goose: пересоздание таблиц при включённых FK, backfill ULID с timestamp из created_at (хронология id сохранена), разнос infohash, удаление idempotency_key. BREAKING: формат id в URL/логах/Telegram, REST-поля id (string) и infohashes (список). Новая конвенция docs/conventions/database.md (без числовых PK), корреляция в логах grep'ом по голому ULID, ER-схема обновлена. Спеки: новая capability identity, MODIFIED в state-reconciliation; change заархивирован. Пройдены ревью дизайна и кода (по 8 углов), все находки исправлены с регрессионными тестами. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,12 +12,13 @@ import (
|
||||
// --- Страница просмотра одной загрузки ---
|
||||
|
||||
type downloadDetailView struct {
|
||||
ID int64
|
||||
ID string
|
||||
Title string
|
||||
SourceType string // тип источника (magnet/torrent/url) — блок «Информация о торренте»
|
||||
SourceFull string // полный источник (magnet) — блок «Информация о торренте»
|
||||
Infohash string
|
||||
Infohash string // первый хеш (для шапки)
|
||||
InfohashShort string
|
||||
Infohashes []string // все хеши загрузки (блок «Информация о торренте»)
|
||||
Context string
|
||||
State string
|
||||
Error string
|
||||
@@ -65,7 +66,8 @@ func detailTitle(d store.Download, rd *worker.ReviewData) string {
|
||||
func (s *server) handleDownload(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := pathID(r)
|
||||
if err != nil {
|
||||
http.Error(w, "некорректный id", http.StatusBadRequest)
|
||||
// Невалидный id = несуществующая сущность; в БД не ходим.
|
||||
http.Error(w, "задача не найдена", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
rd, err := s.deps.Reviewer.ReviewData(r.Context(), id)
|
||||
@@ -85,8 +87,9 @@ func (s *server) handleDownload(w http.ResponseWriter, r *http.Request) {
|
||||
Title: detailTitle(d, rd),
|
||||
SourceType: string(d.SourceType),
|
||||
SourceFull: d.SourceRef,
|
||||
Infohash: d.Infohash.String,
|
||||
InfohashShort: shortenHash(d.Infohash.String),
|
||||
Infohash: d.PrimaryInfohash(),
|
||||
InfohashShort: shortenHash(d.PrimaryInfohash()),
|
||||
Infohashes: d.HashList(),
|
||||
Context: d.Context,
|
||||
State: string(d.State),
|
||||
Error: d.ErrorMsg.String,
|
||||
@@ -126,7 +129,7 @@ func (s *server) handleDownload(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Живая статистика раздачи — со значениями уже в первом кадре; секция
|
||||
// деградирует (пустой контейнер), если торрент не сидирует/данных нет.
|
||||
l, ok := s.deps.Live.Live(d.Infohash.String)
|
||||
l, ok := s.liveFor(d)
|
||||
view.Seeding = buildSeeding(id, l, ok)
|
||||
|
||||
s.render(w, "download.html", view)
|
||||
|
||||
Reference in New Issue
Block a user