Хранилище: нечитаемый файл — «файла нет» с предупреждением; пустой SDLC_HUB_HOME — по умолчанию (#129, #49 M-3, #102 T3.2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f1dae5a811
commit
75b1559354
@ -3,8 +3,10 @@
|
||||
каталог передаётся параметром (Hub.files_dir, решение 10)."""
|
||||
import hashlib
|
||||
import os
|
||||
import logging
|
||||
import re
|
||||
import shutil
|
||||
import stat as stat_mod
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
@ -12,6 +14,8 @@ from pathlib import Path
|
||||
from . import registry
|
||||
from .paths import secure_dir
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MAX_SIZE = 10 * 1024 * 1024 # 10 485 760 байт после декодирования (решение 1)
|
||||
KEEP_DAYS = 30 # возраст по mtime, после которого файл без ссылок убирается (решение 2)
|
||||
SHA_RE = re.compile(r"[0-9a-f]{64}")
|
||||
@ -41,22 +45,32 @@ def exists(root, sha) -> bool:
|
||||
|
||||
|
||||
def size(root, sha) -> int | None:
|
||||
"""Размер файла в байтах; None — файла нет или sha негодный."""
|
||||
"""Размер файла в байтах; None — файла нет (в том числе на его месте каталог), он не читается или sha негодный."""
|
||||
if not is_sha(sha):
|
||||
return None
|
||||
target = path(root, sha)
|
||||
try:
|
||||
return path(root, sha).stat().st_size
|
||||
st = target.stat()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
except OSError as e:
|
||||
log.warning("файл хранилища %s не читается: %s", target, e)
|
||||
return None
|
||||
return st.st_size if stat_mod.S_ISREG(st.st_mode) else None
|
||||
|
||||
|
||||
def read(root, sha) -> bytes | None:
|
||||
"""Содержимое файла; None — файла нет (в том числе на его месте каталог), он не читается или sha негодный."""
|
||||
if not is_sha(sha):
|
||||
return None
|
||||
target = path(root, sha)
|
||||
try:
|
||||
return path(root, sha).read_bytes()
|
||||
return target.read_bytes()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
except OSError as e:
|
||||
log.warning("файл хранилища %s не читается: %s", target, e)
|
||||
return None
|
||||
|
||||
|
||||
def _intact(target, sha) -> bool:
|
||||
|
||||
@ -7,7 +7,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def home() -> Path:
|
||||
return Path(os.environ.get("SDLC_HUB_HOME", str(Path.home() / ".local" / "share" / "sdlc-hub")))
|
||||
return Path(os.environ.get("SDLC_HUB_HOME") or str(Path.home() / ".local" / "share" / "sdlc-hub"))
|
||||
|
||||
|
||||
def legacy_home() -> Path:
|
||||
|
||||
@ -807,6 +807,19 @@ def test_attachment_flow_over_api(hub):
|
||||
assert c.get(f"/batches/{retry['id']}")["commands"][0]["params"] == {"key": "A-1", "file": sha, "filename": "ОСВ 60.xlsx"}
|
||||
|
||||
|
||||
def test_claim_with_directory_instead_of_file_has_size_none(hub):
|
||||
"""Файл вложения заменён каталогом: claim отвечает 200, size — None (#129, #49 M-3)."""
|
||||
c, a = hub["client"], hub["agent"]
|
||||
sha = c.post("/files", {"content": _b64(b"abc")})["file"]
|
||||
c.post("/batches", {"title": "x", "commands": [{"type": "ensure_attachment", "params": {
|
||||
"key": "A-1", "file": sha, "filename": "a.xlsx"}}]})
|
||||
target = hub["hub"].files_dir / sha
|
||||
target.unlink()
|
||||
target.mkdir()
|
||||
cmd = a.post("/agent/claim", {})["command"]
|
||||
assert cmd["params"]["size"] is None
|
||||
|
||||
|
||||
def test_issues_hide_closed(hub):
|
||||
c = hub["client"]
|
||||
hub["agent"].post("/agent/state", {"issues": [{"key": "A-1", "summary": "Т", "status": "В работе", "labels": []},
|
||||
|
||||
@ -189,3 +189,31 @@ def test_sweep_downloads(tmp_path):
|
||||
assert files.sweep_downloads(root) == ["31337"]
|
||||
assert not old.exists() and fresh.exists() and (root / "loose.txt").exists()
|
||||
assert files.sweep_downloads(tmp_path / "нет") == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("func,attr", [(files.size, "stat"), (files.read, "read_bytes")])
|
||||
def test_unreadable_file_is_none_with_warning(root, monkeypatch, caplog, func, attr):
|
||||
from pathlib import Path
|
||||
sha = files.put(root, b"abc")[0]
|
||||
|
||||
def denied(self, *args, **kwargs):
|
||||
raise PermissionError(13, "Permission denied")
|
||||
monkeypatch.setattr(Path, attr, denied)
|
||||
with caplog.at_level("WARNING"):
|
||||
assert func(root, sha) is None
|
||||
assert sha in caplog.text and "не читается" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("func", [files.size, files.read])
|
||||
def test_directory_instead_of_file_is_none(root, caplog, func):
|
||||
(root / ABC).mkdir(parents=True)
|
||||
with caplog.at_level("WARNING"):
|
||||
assert func(root, ABC) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("func", [files.size, files.read])
|
||||
def test_missing_file_is_none_without_log(root, caplog, func):
|
||||
root.mkdir()
|
||||
with caplog.at_level("WARNING"):
|
||||
assert func(root, ABC) is None
|
||||
assert caplog.text == ""
|
||||
|
||||
@ -34,3 +34,10 @@ def test_env_home(monkeypatch, tmp_path):
|
||||
from sdlc_hub.core import paths
|
||||
monkeypatch.setenv("SDLC_HUB_HOME", str(tmp_path / "x"))
|
||||
assert paths.home() == tmp_path / "x"
|
||||
|
||||
|
||||
def test_empty_env_home_is_default(monkeypatch, tmp_path):
|
||||
from sdlc_hub.core import paths
|
||||
monkeypatch.setenv("SDLC_HUB_HOME", "")
|
||||
monkeypatch.setenv("HOME", str(tmp_path))
|
||||
assert paths.home() == tmp_path / ".local" / "share" / "sdlc-hub"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user