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