- выбор → поимённое подтверждение → отчёт: пачка до 20 загрузок, гарды входа на обеих границах, потолок времени и остановка после трёх подряд отказов внешнего сервиса - допуск полного удаления сведён в единую точку store.State.CanDelete() — worker, страница загрузки и Telegram больше не держат своих перечней
9.6 KiB
MODIFIED Requirements
Requirement: Полное удаление загрузки пользователем
Система SHALL предоставлять пользователю команду «Удалить» (delete),
доступную из состояний done, orphaned и target_missing во всех транспортах
(веб-UI и Telegram, опц. REST). Команда SHALL снимать обе стороны загрузки —
целевые библиотечные хардлинки И раздачу с файлами в qBittorrent — и переводить
задачу в терминальное deleted. Из прочих состояний команда доступна SHALL NOT.
Перечень состояний, из которых удаление допустимо, SHALL иметь единственный дом — предикат состояния в модели данных. Транспорты, решающие, показывать действие или нет, SHALL опираться на него, а не на собственный список. Проверку допуска в ядре это SHALL NOT отменять: транспорт решает, что показать, ядро — что допустить, и допуск SHALL держаться без транспорта.
Снятие цели SHALL идти по механике снятия ссылок последнего батча (как в Undo:
superseded пропускаются как забранные другой загрузкой), но отдельным путём с
выключенным гардом последней копии — не переиспользуя guarded-Undo: в отличие
от Undo, delete SHALL снимать целевую ссылку, даже если она — последняя копия
данных (nlink <= 1). Это осознанный выход за
инвариант «источник неприкосновенен», поэтому delete SHALL требовать явного
подтверждения пользователя перед выполнением и SHALL NOT срабатывать по
одиночному клику/тапу. Снятие цели SHALL затрагивать только собственные ссылки
загрузки строго под paths.movies/series; файлы источника под
paths.downloads система сама трогать SHALL NOT — их удаляет qBittorrent по
вызову API с deleteFiles=true.
Подтверждение SHALL быть допустимо одно на пачку загрузок, когда транспорт даёт групповое удаление: ослаблением требования это не является, если подтверждение называет каждую загрузку пачки поимённо. Условия допустимости групповой путь смягчать SHALL NOT — каждая загрузка пачки проходит те же проверки и тот же отказ по конфликту, что и при поштучном удалении, а отказ на одной остальных отменять SHALL NOT.
В отличие от прочих команд, требующих источника, delete синхронный source-preflight
выполнять SHALL NOT и под требование «Принудительная проверка источника/цели перед
действием» не подпадает: цель delete — снять источник, поэтому его отсутствие
трактуется как уже снятая сторона, а не как повод привести состояние сверкой и
отказать. Удаление SHALL быть идемпотентным к отсутствующей стороне: в orphaned
(нет источника) отсутствие раздачи в qBittorrent ошибкой считаться SHALL NOT; в
target_missing (нет цели) пустой список живых ссылок обрабатывается как «нечего
снимать». Если qBittorrent вернул ошибку при удалении присутствующей раздачи,
система в deleted переходить SHALL NOT (не заявляем освобождение места, которого
не произошло), SHALL сообщить пользователю причину отказа (это не ErrConflict,
а ошибка внешнего сервиса — транслируется как таковая), и повторный delete
идемпотентно дожимает удаление, опираясь на оставшийся done либо приведённый
сверкой к реальности target_missing (кратковременное рассогласование до тика
сверки ожидаемо).
Инициатора перехода в deleted система SHALL отличать от фоновой сверки:
пользовательское удаление SHALL помечаться error_code = "user_delete" (сверка
кладёт "reconcile"), человекочитаемую причину — в error_msg и лог перехода.
Новый статус для этого система вводить SHALL NOT — переиспользуется существующее
терминальное deleted (сверка его не переоценивает, см. требование о deleted).
Scenario: Удаление из done снимает обе стороны и освобождает место
- GIVEN задача в
done: раздача присутствует в qBittorrent, её библиотечные хардлинки существуют - WHEN пользователь подтверждает «Удалить»
- THEN библиотечные ссылки последнего батча снимаются
- AND раздача с файлами удаляется из qBittorrent (
deleteFiles=true) - AND задача переходит в
deletedсerror_code = "user_delete"
Scenario: Удаление из orphaned снимает последнюю копию осознанно
- GIVEN задача в
orphaned: источник пропал, библиотечный хардлинк остался единственной копией данных (nlink <= 1) - WHEN пользователь подтверждает «Удалить»
- THEN библиотечная ссылка снимается несмотря на то, что она последняя копия
(гард последней копии выключен, в отличие от
Undo) - AND отсутствие раздачи в qBittorrent ошибкой не считается
- AND задача переходит в
deleted
Scenario: Удаление из target_missing сносит остаточную раздачу
- GIVEN задача в
target_missing: источник присутствует, цель уже удалена вручную - WHEN пользователь подтверждает «Удалить»
- THEN снятие цели идемпотентно (живых ссылок нет)
- AND раздача с файлами удаляется из qBittorrent
- AND задача переходит в
deleted
Scenario: Удаление требует подтверждения
- GIVEN задача в
done - WHEN пользователь инициирует «Удалить», но не подтверждает действие
- THEN ни ссылки, ни раздача не удаляются, состояние остаётся
done
Scenario: Удаление недоступно из прочих состояний
- GIVEN задача в
review(или ином состоянии внеdone/orphaned/target_missing) - WHEN приходит команда «Удалить»
- THEN команда отклоняется с конфликтом, состояние не меняется
Scenario: Ошибка qBittorrent не метит deleted ложно
- GIVEN задача в
done, раздача присутствует, но qBittorrent вернул ошибку на удаление - WHEN пользователь подтверждает «Удалить»
- THEN задача в
deletedне переходит (место не освобождено) - AND пользователю сообщается причина отказа (ошибка qBittorrent, не тихий успех)
- AND повторный delete идемпотентно дожимает удаление
Scenario: Одно подтверждение на пачку не смягчает допуска
- GIVEN транспорт даёт групповое удаление, и человек подтвердил пачку, где каждая загрузка названа поимённо
- WHEN одна из загрузок пачки находится в состоянии, из которого удаление недоступно
- THEN по ней приходит тот же отказ по конфликту, что и при поштучном удалении
- AND остальные загрузки пачки удаляются