0065. Git LFS с блокировками поверх артефактов
Статус: accepted. Owner: Ian Panaev. Дата: 2026-10-04. Второй протокол рядом с /api/v1 после реестра образов (ADR 0063); хранение и права — прежние.
Контекст
Основные проекты — игры на Unity и Unreal. Их ассеты (текстуры, модели, уровни, .uasset) хранят в Git LFS, а хостинги git ограничивают объём и трафик LFS. Ассеты не сливаются построчно, поэтому команда блокирует файл на время правки (File Locking API). Arkvory уже умеет то, что нужно LFS-серверу: потоковую передачу больших объектов, проверку SHA-256, квоты, резервные копии и зеркала. Анализ форматов (ADR 0064) поставил LFS сразу после raw-файлов.
Решение
Адрес. Каждый репозиторий Arkvory — LFS-сервер по адресу https://<host>/lfs/<repository>, это lfs.url git-репозитория (в .lfsconfig или git config). Сам git-репозиторий остаётся на прежнем хостинге: Arkvory хранит только объекты LFS и блокировки.
Протокол — подмножество спецификации git-lfs:
POST …/objects/batch: операцияuploadилиdownload, до 1000 объектов, только адаптерbasicиsha256. Иначе422, а для другого алгоритма —409.- Для upload объекты, которые уже есть (тот же oid и размер), идут без действий: клиент их не передаёт. Для download отсутствующий объект получает ошибку
404внутри ответа. PUT …/objects/<oid>: тело объекта сContent-Length. Хранилище артефактов сверяет размер и SHA-256 с oid, при несовпадении —422, ничего не сохраняется. Уже сохранённый объект не передаётся повторно, а соединение закрывается.GET|HEAD …/objects/<oid>: скачивание с Range, через общую очередь и ограничение полосы.- Блокировки:
POST …/locksсоздаёт блокировку; путь уже заблокирован —409с этой блокировкой.GET …/locks— список с фильтром по пути или id, постранично.POST …/locks/verify— «наши» и «чужие» перед push.POST …/locks/<id>/unlock;forceснимает чужую.
- Ошибки — документ
{message, request_id}с типомapplication/vnd.git-lfs+json. - Маршруты
/lfsследуют спецификации git-lfs и в OpenAPI/api/v1не входят.
Вход. Как у реестра образов: ключ Arkvory (сервисный, персональный токен или файловый) — пароль Basic, имя не проверяется; Bearer тоже принимается. На 401 сервер отвечает LFS-Authenticate: Basic realm="Arkvory", и git спрашивает учётные данные у своего хранилища (Git Credential Manager и т. п.). В действиях передачи (actions.*.header) клиенту возвращается его же Authorization: git-lfs отправляет с ним объект на тот же сервер по тому же TLS. Нужен HTTPS.
Права:
- скачивание и batch download —
content.read; - загрузка и batch upload —
upload.create(файловые ключи —write); - создать, проверить и снять свою блокировку —
upload.create; список —artifact.list; - снять чужую блокировку (
force) —artifact.delete, как удаление артефактов; - зеркало и токен только для чтения загрузки и блокировки не принимают;
- batch — POST и для скачивания, поэтому общий запрет POST для токена только для чтения его пропускает. Операцию
uploadпо-прежнему отклоняет проверка прав: у такого токена их нет.
Хранение.
- Объект — обычный артефакт: имя и SHA-256 — oid, метка
lfs. Идемпотентность загрузки — по oid, поэтому повтор после обрыва продолжает тот же артефакт. - Миграция 31 (только расширение схемы):
arkvory_lfs_objects(repository, oid → artifact)иarkvory_lfs_locks(id, repository, path, owner_id, owner_name, locked_at), один замок на путь в репозитории. - Имя владельца в блокировке — имя пользователя или сервисного аккаунта на момент блокировки, у файлового ключа — его ID.
- Обе таблицы входят в резервные копии.
- Объекты LFS защищены от retention причиной
reference: сервер не видит историю git, поэтому объект хранится, пока его не удалит администратор.
Зеркала. Запись объекта пишет в ленту действие lfs.object (деталь — oid) в своей транзакции, под замком каталога репозитория. Зеркало после копии артефакта добавляет строку в свой индекс, а после засева проигрывает эти записи, как записи реестра. Блокировки не зеркалируются: в зеркале нечего блокировать.
Последствия
- Удаления объектов и сборки неиспользуемых объектов нет. Это осознанно: на неверное удаление LFS отвечает сломанной историей.
- Адаптер
tus(докачка внутри объекта) не поддержан: git-lfs повторяет объект целиком. Отдельный объект больше сотен ГБ лучше держать в raw-файлах или UPack. - Шлюзы чтения (ADR 0009) LFS не обслуживают: batch — это POST, поэтому
lfs.urlуказывает на пишущий шлюз. - Нет: SSH-входа (
git-lfs-authenticate), прокси к чужому LFS-серверу, блокировок по веткам — они общие на репозиторий.
Проверки
tests/integration/git-lfs.test.mjsс настоящимиgitиgit-lfsв изолированной конфигурации:pushдвух больших файлов иcloneв другой каталог со сверкой SHA-256;- повторный
pushбез новых объектов; lock,locks, повторныйlockотклонён,unlock;- через HTTP: 401 с
LFS-Authenticate, отсутствующий объект, неверный oid,sha1, тело не по oid, нехватка прав.
tests/integration/mirror-images.test.mjs: объекты LFS до и после засева скачиваются с зеркала, загрузка в зеркало отклоняется.tests/mirror-sync.test.mjs: проигрышlfs.object.- В образ локального CI добавлен
git-lfs; на раннерах GitHub он уже есть.
Дополнение: проверка перед 0.4.0
- Тела объектов принимаются при любом Content-Type. git-lfs определяет тип по содержимому, и текстовые ассеты (Unity Force Text:
.prefab,.unity,.asset) приходят какtext/plain. Раньше такие загрузки отклонялись. - Если загрузка под ключом
lfs-<oid>истекла или отменена, объект загружается заново под новым ключом. Раньше одна оборванная попытка блокировала объект для владельца навсегда. - Наличие объекта проверяется по индексу без
content.read: ключи только на запись видят, что объект уже есть. - Ссылки действий строятся из адреса, по которому пришёл клиент.
X-Forwarded-Proto: httpsможет только повысить схему.Authorizationклиента возвращается лишь в ссылку https или loopback и никогда не уходит в открытый HTTP к другому хосту. - Страницы блокировок сравнивают курсор в порядке байтов. ID блокировки — строго UUID, иначе она просто неизвестна.