Принимаем .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>
7.0 KiB
7.0 KiB
1. Парсер internal/torrent
- 1.1 Добавить зависимость
github.com/anacrolix/torrent(пакетmetainfo);task tidy - 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_v2qBit), для v2-only хранить именно v2. Уточнить точный v2-API anacrolix при apply - 1.3
func (Info) Context() string— строки-факты (\n): имя раздачи,Размер:изTotalLength, сигнал по файлам (число/расширения),Трекер:домен из announce/announce-list, комментарий (если содержателен). Без сети - 1.4 Хелперы размера/домена: дублировать тривиальные из
internal/magnetили вынести общий (решение по месту) - 1.5 Тесты: одиночный файл; много файлов; гибрид v1+v2; трекеры из
announce-list; повреждённые/не-torrent байты → ошибка;
Context()— содержательный и пустой случай; отсутствие сетевых вызовов
2. Хранилище: байты торрента
- 2.1 Goose-миграция: таблица
download_torrent(download_id TEXT PRIMARY KEY REFERENCES download(id) ON DELETE CASCADE, data BLOB NOT NULL) - 2.2 Обновить ER-схему
docs/specs/database.md(новая таблица + связь) - 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) - 2.4 Тесты store: запись/чтение байтов; при дедупе байты не пишутся;
CASCADE-уборка при (гипотетическом) удалении загрузки. Байты НЕ удаляются при
промоушене
catched → downloading(нужны для retry)
3. Приём: ветка байтов в ingest
- 3.1 Расширить
ingest.RequestполемTorrentData []byte(опц.) - 3.2 В
Ingest: при непустомTorrentData—torrent.Parse,SourceType = SourceTorrent,SourceRef= имя раздачи/файла (не URL), контекст =mergeContext(req.Context, tinfo.Context()), запись байтов (2.3) в транзакции создания; иначе — прежний magnet-путь без изменений - 3.3 Ограничение размера входа на границе (константа, дефолт напр. 8 MiB)
- 3.4 Тесты ingest: приём
.torrent→catched,source_type=torrent, инфохэши, байты сохранены, контекст синтезирован; дедуп по инфохэшу (байты не пишутся); превышение лимита → ошибка
4. Воркер: диспетч добавления по типу источника (оба add-пути)
- 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}, …})) - 4.2 Применить хелпер в
processCatched(worker.go:359-365) И вRetry(worker.go:677-691): снять magnet-only гейт в Retry — torrent без живой раздачи должен добавляться байтами, а не активироваться безAdd. Сохранить откат активации при сбое add в Retry - 4.3 Общими остаются: ре-валидация перехода, обработка сбоя
add(processCatched— остаётсяcatched; Retry — откат), namer/qbt вне блокировки - 4.4 Тесты воркера: (а)
catchedtorrent →qbt.AddсTorrents(неURLs), переход вdownloading, сбой add → остаётсяcatched; (б)Retrytorrent без живой раздачи →AddсTorrents, переход вdownloading; сбой add → откат в прежнее состояние
5. Транспорт: веб-форма (multipart)
- 5.1 Шаблон формы:
enctype="multipart/form-data",<input type="file" name="torrent" accept=".torrent,application/x-bittorrent">рядом со строкой источника; строка источника не обязательна при выбранном файле - 5.2
handleUIAdd:http.MaxBytesReader/лимит,ParseMultipartForm; есть файлtorrent→Ingest(Request{TorrentData, Context}); иначе — прежний путь. PRG-редирект/htmx как сейчас - 5.3 Деградация без JS: обычный multipart-POST приводит к тому же результату
- 5.4 Тест обработчика: multipart с
.torrent→ загрузка заведена; без файла → прежний текстовый путь; превышение размера → ошибка
6. Транспорт: Telegram-документ
- 6.1 В
handleMessageобрабатыватьm.Documentдо ветки pending-hint и текстовой ветки (у документаm.Textпуст): mimeapplication/x-bittorrentили расширение.torrent→ скачать байты (GetFileDirectURL+ HTTP GET, лимит),Ingest(Request{TorrentData, Context: m.Caption}) - 6.2 Документ не-
.torrent→ прежний ответ-подсказка; сохранить allowlist-проверку.AllowedUpdatesне менять (документ — внутриmessage) - 6.3 Тест парса/маршрутизации документа (mime/расширение → ветка байтов); скачивание — за интерфейсом/мок
7. Проверка
- 7.1
task testиtask lintзелёные - 7.2
openspec validate torrent-file-ingest --strictпроходит - 7.3 Ручная проверка:
.torrentчерез веб-пикер и Telegram-документ →catched → downloading, задача видна в qBittorrent сrename