Files
avandClaude Opus 4.8 14d615a7c2 Приём: добавление загрузки по .torrent-файлу
Принимаем .torrent как загруженные байты — через файл-пикер в веб-форме и
Telegram-документ, наряду с magnet. Файл несёт полные метаданные: работает
там, где magnet не резолвится (закрытые трекеры, без DHT), и даёт максимум
контекста для распознавания без сети.

- internal/torrent: парсер поверх anacrolix/torrent/metainfo — инфохэш(и)
  (v1 SHA1 исходных байтов info; v2 BEP52 при наличии) + Context() из имени,
  дерева файлов, размера, трекеров. Извлечение файлов панико-безопасно
  (недоверенный вход).
- Персистентность байтов: таблица-спутник download_torrent (миграция 0009);
  пишется в транзакции создания загрузки, только на ветке создания (не при
  дедупе). Байты живут весь срок строки — нужны для повторного добавления
  при retry.
- ingest: Request.TorrentData/TorrentName, диспетч парсера; source_ref —
  человекочитаемый референс (имя раздачи/файла), не адрес добавления.
- worker: общий sourceAddParts ветвит по source_type в ОБОИХ add-путях —
  processCatched и Retry (torrent добавляется файлом, не magnet-хешем).
- Транспорты: multipart-форма с файл-пикером (деградация без JS) и приём
  Telegram-документа (скачивание с редактированием токена из ошибок — секрет
  не в логи; обработка до ветки pending/текста).

Разработка по OpenSpec (SDD): change torrent-file-ingest, два чекпоинта ревью
(дизайн до кода, код до архива) сабагентами; дельты влиты в спеки, change
архивирован. Ручная проверка на живом qBittorrent (7.3) — за деплоем.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 10:52:31 +03:00

94 lines
7.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 1. Парсер `internal/torrent`
- [x] 1.1 Добавить зависимость `github.com/anacrolix/torrent` (пакет
`metainfo`); `task tidy`
- [x] 1.2 Пакет `internal/torrent`: `Parse(data []byte) (Info, error)` через
`metainfo.Load`/`UnmarshalInfo`; `Info{Infohash, Infohashes, DisplayName,
Files, TotalLength, Trackers, Comment}`. Инфохэш v1 — из `HashInfoBytes`
(SHA1 исходных байтов info, нижний hex); v2/гибрид — извлечь v2 (64-hex,
как `infohash_v2` qBit), для v2-only хранить именно v2. Уточнить точный v2-API
anacrolix при apply
- [x] 1.3 `func (Info) Context() string` — строки-факты (`\n`): имя раздачи,
`Размер:` из `TotalLength`, сигнал по файлам (число/расширения), `Трекер:`
домен из announce/announce-list, комментарий (если содержателен). Без сети
- [x] 1.4 Хелперы размера/домена: дублировать тривиальные из `internal/magnet`
или вынести общий (решение по месту)
- [x] 1.5 Тесты: одиночный файл; много файлов; гибрид v1+v2; трекеры из
announce-list; повреждённые/не-torrent байты → ошибка; `Context()`
содержательный и пустой случай; отсутствие сетевых вызовов
## 2. Хранилище: байты торрента
- [x] 2.1 Goose-миграция: таблица `download_torrent(download_id TEXT PRIMARY KEY
REFERENCES download(id) ON DELETE CASCADE, data BLOB NOT NULL)`
- [x] 2.2 Обновить ER-схему `docs/specs/database.md` (новая таблица + связь)
- [x] 2.3 Изменить сигнатуру `CreateDownloadIfNoActive(ctx, d, hashes,
torrentBlob []byte)`: на ветке создания `INSERT INTO download_torrent` в той
же tx (при дедупе — не пишет). Обновить интерфейсы `Store` в `internal/ingest`
и `internal/worker` и вызовы magnet-приёма/`discover.go` (передают `nil`).
Добавить `GetTorrentData(ctx, downloadID) ([]byte, error)`
- [x] 2.4 Тесты store: запись/чтение байтов; при дедупе байты не пишутся;
CASCADE-уборка при (гипотетическом) удалении загрузки. Байты НЕ удаляются при
промоушене `catched → downloading` (нужны для retry)
## 3. Приём: ветка байтов в `ingest`
- [x] 3.1 Расширить `ingest.Request` полем `TorrentData []byte` (опц.)
- [x] 3.2 В `Ingest`: при непустом `TorrentData` — `torrent.Parse`,
`SourceType = SourceTorrent`, `SourceRef` = имя раздачи/файла (не URL),
контекст = `mergeContext(req.Context, tinfo.Context())`, запись байтов (2.3)
в транзакции создания; иначе — прежний magnet-путь без изменений
- [x] 3.3 Ограничение размера входа на границе (константа, дефолт напр. 8 MiB)
- [x] 3.4 Тесты ingest: приём `.torrent` → `catched`, `source_type=torrent`,
инфохэши, байты сохранены, контекст синтезирован; дедуп по инфохэшу (байты не
пишутся); превышение лимита → ошибка
## 4. Воркер: диспетч добавления по типу источника (оба add-пути)
- [x] 4.1 Общий хелпер `addSource(ctx, d, rename)`: ветвь по `d.SourceType` —
magnet/url (`URLs`, hint из `magnet.Parse`); torrent (`GetTorrentData`, hint
из `torrent.Parse(data).DisplayName`, `qbt.Add(AddRequest{Torrents:
[][]byte{data}, …})`)
- [x] 4.2 Применить хелпер в `processCatched` (worker.go:359-365) И в `Retry`
(worker.go:677-691): снять magnet-only гейт в Retry — torrent без живой
раздачи должен добавляться байтами, а не активироваться без `Add`. Сохранить
откат активации при сбое add в Retry
- [x] 4.3 Общими остаются: ре-валидация перехода, обработка сбоя `add`
(`processCatched` — остаётся `catched`; Retry — откат), namer/qbt вне
блокировки
- [x] 4.4 Тесты воркера: (а) `catched` torrent → `qbt.Add` с `Torrents` (не
`URLs`), переход в `downloading`, сбой add → остаётся `catched`; (б) `Retry`
torrent без живой раздачи → `Add` с `Torrents`, переход в `downloading`; сбой
add → откат в прежнее состояние
## 5. Транспорт: веб-форма (multipart)
- [x] 5.1 Шаблон формы: `enctype="multipart/form-data"`, `<input type="file"
name="torrent" accept=".torrent,application/x-bittorrent">` рядом со строкой
источника; строка источника не обязательна при выбранном файле
- [x] 5.2 `handleUIAdd`: `http.MaxBytesReader`/лимит, `ParseMultipartForm`; есть
файл `torrent` → `Ingest(Request{TorrentData, Context})`; иначе — прежний
путь. PRG-редирект/htmx как сейчас
- [x] 5.3 Деградация без JS: обычный multipart-POST приводит к тому же
результату
- [x] 5.4 Тест обработчика: multipart с `.torrent` → загрузка заведена; без
файла → прежний текстовый путь; превышение размера → ошибка
## 6. Транспорт: Telegram-документ
- [x] 6.1 В `handleMessage` обрабатывать `m.Document` **до** ветки pending-hint
и текстовой ветки (у документа `m.Text` пуст): mime `application/x-bittorrent`
или расширение `.torrent` → скачать байты (`GetFileDirectURL` + HTTP GET,
лимит), `Ingest(Request{TorrentData, Context: m.Caption})`
- [x] 6.2 Документ не-`.torrent` → прежний ответ-подсказка; сохранить
allowlist-проверку. `AllowedUpdates` не менять (документ — внутри `message`)
- [x] 6.3 Тест парса/маршрутизации документа (mime/расширение → ветка байтов);
скачивание — за интерфейсом/мок
## 7. Проверка
- [x] 7.1 `task test` и `task lint` зелёные
- [x] 7.2 `openspec validate torrent-file-ingest --strict` проходит
- [ ] 7.3 Ручная проверка: `.torrent` через веб-пикер и Telegram-документ →
`catched → downloading`, задача видна в qBittorrent с `rename`