- выбор → поимённое подтверждение → отчёт: пачка до 20 загрузок, гарды входа на обеих границах, потолок времени и остановка после трёх подряд отказов внешнего сервиса - допуск полного удаления сведён в единую точку store.State.CanDelete() — worker, страница загрузки и Telegram больше не держат своих перечней
116 lines
9.6 KiB
Markdown
116 lines
9.6 KiB
Markdown
## 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** остальные загрузки пачки удаляются
|