layout: непомещающееся целевое имя уводит задачу в review вместо failed
- предел длины компонента (255 байт) проверяется в BuildLinks до первой операции с ФС: ни каталога, ни ссылки при отказе не создаётся - причина пустого предпросмотра считается на показе (ReviewData.PreviewError) и печатается в панели действий и в карточке Telegram: у задачи без записанной причины взять её больше неоткуда
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
## Context
|
||||
|
||||
`layout.BuildLinks` — чистая функция от плана: строит целевые пути, проверяет
|
||||
выход за библиотеку и не трогает диск. Длину имени она не проверяет, поэтому
|
||||
слишком длинное название доезжает до `Apply`, где `os.MkdirAll` или `os.Link`
|
||||
получают от ядра `ENAMETOOLONG`. `worker.linkPlan` разбирает исход `Apply` двумя
|
||||
ветками: `layout.ErrCollision` → `review`, всё остальное → `failed` с текстом
|
||||
ошибки в `error_msg`. Системная ошибка попадает во вторую.
|
||||
|
||||
Путь «отказ построения → review» в проекте уже есть: ошибка `BuildLinks` уводит
|
||||
задачу в `review` с кодом `build` (`worker/review.go`). То есть чинить надо не
|
||||
маршрут, а место проверки и различимость причины.
|
||||
|
||||
Предел длины на ext4/xfs/btrfs — 255 **байт** на компонент (`NAME_MAX`), а не
|
||||
символов: кириллическое название в UTF-8 тратит по два байта на букву и
|
||||
упирается вдвое раньше, чем кажется по числу знаков.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Непомещающееся имя даёт `review` с доменной причиной, а не `failed` с текстом
|
||||
ядра.
|
||||
- Отказ целиком: ни каталога, ни ссылки. Проверка стоит до первой операции с ФС.
|
||||
- Причина различима кодом — по ней видно, чем этот вход в `review` отличается от
|
||||
коллизии и от прочих отказов построения.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- **Не обрезаем и не переименовываем название сами.** Это решение человека, и
|
||||
подсказка названия на ревью для него уже есть. Автоматическая обрезка дала бы
|
||||
папку, которую правило сходимости потом унаследует навсегда.
|
||||
- **Не выясняем предел у ядра** (`pathconf(_PC_NAME_MAX)`, пробная запись).
|
||||
Проверка обязана отвечать до обращения к диску, а на разных подкаталогах
|
||||
ответы могут различаться.
|
||||
- **Не проверяем предел длины пути целиком** (`PATH_MAX`, 4096 байт). Корни
|
||||
библиотек задаёт оператор, глубина у нас фиксированная (папка тайтла + папка
|
||||
сезона + файл), и трёх компонентов по 255 байт до 4096 не хватает.
|
||||
|
||||
## Decisions
|
||||
|
||||
**Решение 1. Проверка живёт в `BuildLinks`, по компонентам относительного
|
||||
пути.** `BuildLinks` уже единственное место, где целевой путь собирается целиком
|
||||
и проверяется на выход за библиотеку, — вторая проверка того же пути встаёт
|
||||
рядом с первой. Проверяются компоненты **под корнем библиотеки**, а не весь путь:
|
||||
корни задаёт оператор, и жаловаться на его каталоги раскладка не вправе.
|
||||
|
||||
**Решение 2. Предел — константа 255 байт с объяснением, откуда взята.**
|
||||
`NAME_MAX` у ext4, xfs и btrfs — 255 байт; это предел самой распространённой
|
||||
раскладки, а не универсальный закон. Значение стоит именованной константой в
|
||||
`internal/layout` с комментарием, называющим источник. В конфиг не выносится:
|
||||
настройка, которую никто не знает, как выставить, — это не гибкость.
|
||||
|
||||
**Решение 3. Своя ошибка `layout.ErrNameTooLong`, обёрнутая как остальные.**
|
||||
`errors.Is` по ней ловит случай на любой глубине обёртки — так же, как
|
||||
`ErrCollision`. Текст ошибки называет компонент и его длину в байтах: без числа
|
||||
человек не поймёт, насколько сокращать.
|
||||
|
||||
**Решение 4. Свой код причины `name_too_long` рядом с `collision`.** Ошибка
|
||||
`BuildLinks` сегодня даёт код `build` — он верен, но объединяет непомещающееся
|
||||
имя с пустым названием и серией без номера. Код причины в этом проекте —
|
||||
корреляционный ключ шага, и слепить три разных отказа в один значит потерять
|
||||
возможность отличить чинимое подсказкой от нечинимого. Ветка встаёт в `linkPlan`
|
||||
перед общей веткой `build`.
|
||||
|
||||
**Решение 5. Проверка длины идёт после проверки выхода за библиотеку.**
|
||||
Путь, вылезший из песочницы, — это `critical`; сообщить о нём длиной имени
|
||||
значило бы скрыть находку безопасности за косметической причиной. Порядок
|
||||
проверок в цикле сохраняет приоритет.
|
||||
|
||||
**Решение 6. Превью получает проверку даром — и это довод за `BuildLinks`.**
|
||||
Оба предпросмотра ревью (карточка и строка источника) строят пути тем же
|
||||
`BuildLinks` (`worker/review.go`), поэтому вердикт на показе и вердикт на
|
||||
применении совпадают по устройству, а не по договорённости. Цена — видимая:
|
||||
непомещающееся имя даёт пустой предпросмотр, а панель действий при пустом
|
||||
предпросмотре прячет «Применить» и печатает общий текст «Подтверди источник».
|
||||
Баннер причины над карточкой при этом верен. Поведение не новое — так уже ведут
|
||||
себя все отказы построения плана, — но это изменение делает его достижимым на
|
||||
штатном входе.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Предел зашит и может не совпасть с реальной ФС.** На ФС с меньшим
|
||||
`NAME_MAX` (например, некоторые зашифрованные) имя пройдёт проверку и упрётся в
|
||||
ядро — тот же `failed`, что сегодня, но уже только на редкой раскладке. На ФС с
|
||||
бо́льшим пределом мы отказываем строже, чем нужно. Обе стороны названы в
|
||||
комментарии константы; лечение — правка константы, а не настройка.
|
||||
- **Причина различима, но не самообъясняема.** Человек видит «имя не
|
||||
помещается» и длину — но не видит, на сколько байт сокращать после
|
||||
provider-тега и суффикса серии. Считать это за него — отдельная работа, здесь
|
||||
она не делается.
|
||||
- **Изменение сдвигает часть нынешних `failed` в `review`.** Это и есть цель, но
|
||||
задачи, лежащие в `failed` с этой ошибкой прямо сейчас, сами туда не переедут и
|
||||
сверкой не подхватятся: из `failed` их выводит только `retry`, а он тянет
|
||||
полный цикл через `downloading` и распознавание. Обещания самоизлечения здесь
|
||||
нет.
|
||||
- **Унаследованная база отказа по длине не даёт, и это арифметика, а не
|
||||
везение.** На чекпоинте эта строка стояла как признанный тупик: база берётся с
|
||||
диска, подсказка её не укорачивает, задача циклится. Попытка написать тест
|
||||
показала, что случай недостижим. Папка-якорь лежит на диске, значит её имя уже
|
||||
не длиннее предела; хвост имени файла — `" S02E01"` плюс расширение, 11 байт —
|
||||
не длиннее хвоста имени папки, `" ["` плюс provider-тег плюс `"]"`, минимум 11
|
||||
байт (`tmdbid-1`). Язык и флаги субтитров и двойные серии в `layout.PlanFile`
|
||||
из распознавания не приходят вовсе (`toLayoutPlan` их не заполняет), поэтому
|
||||
длиннее не станет. Свойство держит тест `TestApply_InheritedBaseAtLimitStillFits`:
|
||||
если суффиксы вырастут, он покраснеет раньше, чем задача упрётся в тупик.
|
||||
Reference in New Issue
Block a user