- предел длины компонента (255 байт) проверяется в BuildLinks до первой операции с ФС: ни каталога, ни ссылки при отказе не создаётся - причина пустого предпросмотра считается на показе (ReviewData.PreviewError) и печатается в панели действий и в карточке Telegram: у задачи без записанной причины взять её больше неоткуда
107 lines
9.7 KiB
Markdown
107 lines
9.7 KiB
Markdown
## 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`:
|
||
если суффиксы вырастут, он покраснеет раньше, чем задача упрётся в тупик.
|