diff --git a/README.md b/README.md index 31a7ef0..a1f13b3 100644 --- a/README.md +++ b/README.md @@ -64,11 +64,14 @@ ack (с `attempt` повторного запроса); файл в ИС вто `` (по умолчанию `Tmp`), затем он публикуется в `Input` под настоящим именем. `Tmp` лежит рядом с `Input` и `Output`, на той же ФС/шаре: публикация атомарна, а ИС не может забрать недописанный файл (временных файлов в `Input` нет — решение владельца 06.10.2026, у FileWorker `.tmp` лежит рядом с назначением). В режиме `local` публикация — - жёсткая ссылка `.part` → `Input/<имя>` (`FileExistsError`, если назначение есть) и удаление `.part`; на файловой - системе без жёстких ссылок (`EPERM`/`EXDEV`/`ENOTSUP`) — `os.replace`, если назначения ещё нет (решение №17). В - режиме `smb` — `smbclient.rename` (поднимает `FileExistsError`, если назначение есть). Если `Input/.` - уже существует (повторная публикация задания после потери ack в другом процессе), файл считается доставленным: - назначение не перезаписывается, задание продолжается опросом `Output//`. Каталоги `Input` и `Tmp` + жёсткая ссылка `.part` → `Input/<имя>` и удаление `.part`; на файловой системе без жёстких ссылок + (`EPERM`/`EXDEV`/`ENOTSUP`/`EOPNOTSUPP` от самой ссылки) — `os.replace`, если назначения ещё нет (решение №17). + `.part` создаётся заново: остаток прежней записи удаляется (ошибка удаления подавляется), файл открывается с `O_EXCL`; + поверх оставшегося `.part` пишем только если это не жёсткая ссылка (`st_nlink == 1`) — иначе исключение (решение №32), + чтобы не испортить уже опубликованный файл. В режиме `smb` — `smbclient.rename` (поднимает `FileExistsError`, если + назначение есть). Если `Input/.` уже существует (повторная публикация задания после потери ack в другом + процессе), файл считается доставленным: ничего не пишется, назначение не перезаписывается, `.part` не трогается (при + `SCAN_CLEANUP=true` существующий `.part` снимается), задание продолжается опросом `Output//` (решение №35). Каталоги `Input` и `Tmp` создаются при необходимости (без родителей), корень горячей папки сам не создаётся: нет корня — `RECOGNITION_FAILED`. - Ответ ИС — каталог `//`: ИС кладёт туда **все** распознанные документы задания одновременно, по одному `*.json` на файл (`Output//*.json`). Шлюз читает **все** `*.json` каталога и склеивает их в один @@ -105,8 +108,8 @@ ack (с `attempt` повторного запроса); файл в ИС вто от `SCAN_CLEANUP` (в `local` — удаление после жёсткой ссылки, ошибка удаления подавляется; в `smb` `.part` переименовывается во входящий и отдельного удаления не требует): иначе при выключенной очистке на каждое задание оставалась бы лишняя ссылка, а ошибка удаления на шаре с неподтверждёнными правами превращала бы доставленный файл в - `RECOGNITION_FAILED`. **Флаг управляет остатками:** осиротевшие `.part` (оборванная запись), `.part` при уже - существующем назначении, каталог ответа, входящий файл и `.done`. При `SCAN_CLEANUP=false` сервис не удаляет ничего из + `RECOGNITION_FAILED`. **Флаг управляет остатками:** осиротевшие `.part` (оборванная запись, `.part` при уже + существующем назначении), каталог ответа, входящий файл и `.done`. При `SCAN_CLEANUP=false` сервис не удаляет ничего из этого и не пишет в лог об осиротевших `.part` (решение №27): они накапливаются в `Tmp` и убираются вручную или включением флага. - Права на SMB: для очистки учётной записи шары нужны права на удаление в `Input`, `Output` (включая каталог diff --git a/openspec/changes/slice-3a-ocr-normalizer/specs/image-recognition-gateway/spec.md b/openspec/changes/slice-3a-ocr-normalizer/specs/image-recognition-gateway/spec.md index 4419026..d40c18b 100644 --- a/openspec/changes/slice-3a-ocr-normalizer/specs/image-recognition-gateway/spec.md +++ b/openspec/changes/slice-3a-ocr-normalizer/specs/image-recognition-gateway/spec.md @@ -129,7 +129,7 @@ `/.`, его вариант `/..done` и временный `/...part`, если они существуют. При `SCAN_CLEANUP=false` (по умолчанию) сервис MUST NOT удалять ничего, включая осиротевшие временные файлы (`.part`, ставший целью успешной публикации, снимается самой записью — это завершение «rename без замены», а не очистка; ошибка его снятия -запись не прерывает, решение №26). При +запись не прерывает, решение №26; запись собственного `.part` заново удаляет прежний остаток под тем же именем, решение №32). При `TIMEOUT`, `FILE_READ_ERROR`, `RECOGNITION_FAILED` записи очистка MUST NOT выполняться. Ошибка удаления SHALL быть записана в лог одной строкой на задание и MUST NOT менять конверт, `metrics.warnings` и `/healthcheck`.