fix(files): повторная загрузка лечит испорченную копию того же размера (M-2, #43)
put сверяет sha256 копии на диске при совпадении размера и переписывает испорченную; design.md: решение 13 уточнено, рулинги R-3…R-5 — решения 21–23 (M-5 финального ревью). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4e397b563d
commit
2e98ce6062
@ -111,9 +111,10 @@ filename="<имя UTF-8>"; filename*=UTF-8''<percent-encoded>`, `Content-Type: a
|
||||
12. 15.10.2026 — `files.path/exists/size/read` принимают только sha из 64 символов `[0-9a-f]`, иначе «файла нет»
|
||||
(`path` — `ValueError`). Обоснование: APFS не различает регистр — `BA78…` нашёл бы `ba78…`; `..` и `/` не строят
|
||||
путь вне `files/`. Цена: нет.
|
||||
13. 15.10.2026 — повторная загрузка: копия того же размера есть — только `os.utime`; другого размера (оборванная
|
||||
запись) — файл переписывается; содержимое по sha256 не перепроверяется. Обоснование: имя — sha256 содержимого,
|
||||
агент сверяет sha256 при скачивании. Цена: подменённый на диске файл того же размера хаб не заметит (заметит агент).
|
||||
13. 15.10.2026 — повторная загрузка: целая копия (тот же размер и sha256 содержимого на диске) — только `os.utime`;
|
||||
оборванная (другой размер) или испорченная (тот же размер, другой sha256 — M-2 финального ревью) — переписывается.
|
||||
Обоснование: имя — sha256 содержимого; повторная загрузка из чата должна лечить испорченную копию. Цена: чтение
|
||||
копии (до 10 МБ) при каждой повторной загрузке.
|
||||
14. 15.10.2026 — `POST /files` отбрасывает пробельные символы (переносы строк) в `content` до строгой проверки base64.
|
||||
Обоснование: GNU `base64` и многие библиотеки переносят строку через 76 символов; спека говорит только «не base64 —
|
||||
400». Цена: строка с пробелами внутри тоже принимается.
|
||||
@ -140,3 +141,11 @@ filename="<имя UTF-8>"; filename*=UTF-8''<percent-encoded>`, `Content-Type: a
|
||||
и LF) — «команда N: filename без кавычек и управляющих символов (CR, LF и др.)» (рулинг R-2). Обоснование: такие
|
||||
имена ломают заголовок части `multipart/form-data` у агента; кодирование имени в агенте остаётся запасной защитой.
|
||||
Цена: файл с кавычкой в имени прикладывается под другим именем (`filename` в команде).
|
||||
21. 16.10.2026 — текст отчёта агента при повреждённом в пути файле («результат неизвестен (обрыв связи с хабом)…»)
|
||||
не меняется (рулинг R-3): общий путь повтора `Runner.execute`. Цена: неточная формулировка в редком случае — в Jira
|
||||
при этом ничего не записано.
|
||||
22. 16.10.2026 — агент без записи `issue_types.<тип>` (в том числе `task` и `epic`) отвечает `failed` «нет настройки
|
||||
issue_types.<тип>» без вызова Jira вместо `{"name": null}` (рулинг R-4, следует из спеки agent). Цена: нет — на
|
||||
живом хабе настройки `task` и `epic` есть; отмечено в PR.
|
||||
23. 16.10.2026 — план агента (5 задач) исполнен параллельно фоновым исполнителем на sonnet по executing-plans, план
|
||||
хаба — в сессии; финальное ревью обеих веток — opus (рулинг R-5). Цена: два контекста вместо одного.
|
||||
|
||||
@ -57,9 +57,17 @@ def read(root, sha) -> bytes | None:
|
||||
return None
|
||||
|
||||
|
||||
def _intact(target, sha) -> bool:
|
||||
"""Копия на диске совпадает по sha256: испорченную того же размера повторная загрузка переписывает (M-2 ревью)."""
|
||||
try:
|
||||
return hashlib.sha256(target.read_bytes()).hexdigest() == sha
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
|
||||
|
||||
def put(root, data: bytes) -> tuple[str, int]:
|
||||
"""Сохранить содержимое, вернуть (sha256, размер). Копия того же размера уже есть — только обновить mtime (уборка
|
||||
считает возраст от последней загрузки); другого размера (оборванная запись) — переписать (решение 13). Запись
|
||||
"""Сохранить содержимое, вернуть (sha256, размер). Целая копия уже есть (тот же sha256 содержимого) — только обновить
|
||||
mtime (уборка считает возраст от последней загрузки); оборванная или испорченная — переписать (решение 13). Запись
|
||||
атомарная: временный файл mkstemp в том же каталоге, fsync, os.replace; при сбое временный файл удаляется
|
||||
(решение 11)."""
|
||||
if not data:
|
||||
@ -70,7 +78,7 @@ def put(root, data: bytes) -> tuple[str, int]:
|
||||
secure_dir(root)
|
||||
sha = hashlib.sha256(data).hexdigest()
|
||||
target = root / sha
|
||||
if size(root, sha) == len(data):
|
||||
if size(root, sha) == len(data) and _intact(target, sha):
|
||||
try:
|
||||
os.utime(target)
|
||||
return sha, len(data)
|
||||
|
||||
@ -147,3 +147,11 @@ def test_sweep_uses_now(root, conn):
|
||||
sha = _file(root, b"N", 0)
|
||||
assert files.sweep(conn, root) == []
|
||||
assert files.sweep(conn, root, now=time.time() + 31 * DAY) == [sha]
|
||||
|
||||
|
||||
def test_put_rewrites_corrupted_copy_of_same_size(root):
|
||||
"""Финальное ревью M-2: испорченная копия того же размера лечится повторной загрузкой."""
|
||||
files.put(root, b"abc")
|
||||
(root / ABC).write_bytes(b"xyz") # тот же размер, другое содержимое
|
||||
assert files.put(root, b"abc") == (ABC, 3)
|
||||
assert (root / ABC).read_bytes() == b"abc"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user