mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-23 10:50:14 +00:00
fix(upload): scope archive extraction to per-session attachment dir
handle_upload_extract() used Path(s.workspace) as the extraction root, bypassing HERMES_WEBUI_ATTACHMENT_DIR entirely. Route through _session_attachment_dir(session_id) so archives land alongside single-file uploads and session cleanup covers them. Add tests and CHANGELOG entry. Ref #2247
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- **PR #2520** by @OneFat3 (refs #2247) — Route archive extraction (`/api/upload/extract`) through the per-session attachment inbox (`_session_attachment_dir`) instead of hardcoded `Path(s.workspace)`, matching the single-file upload path. Extracted archives now land at `<attachment_root>/<session_id>/<archive_stem>/` so session deletion cleanup covers them and per-session isolation is preserved when `HERMES_WEBUI_ATTACHMENT_DIR` is configured.
|
||||
|
||||
## [v0.51.89] — 2026-05-18 — Release BM (stage-382 — 6-PR full sweep batch — runtime adapter approval/clarify seam + SOUL.md memory panel + #1855 resolve_model_provider fast-path + PWA sidebar spinner fix + /model active-provider preference + contributor contract docs index)
|
||||
|
||||
### Changed
|
||||
|
||||
+3
-2
@@ -258,8 +258,9 @@ def handle_upload_extract(handler):
|
||||
s = get_session(session_id)
|
||||
except KeyError:
|
||||
return j(handler, {'error': 'Session not found'}, status=404)
|
||||
workspace = Path(s.workspace)
|
||||
result = extract_archive(file_bytes, filename, workspace)
|
||||
session_dir = _session_attachment_dir(session_id)
|
||||
session_dir.mkdir(parents=True, exist_ok=True)
|
||||
result = extract_archive(file_bytes, filename, session_dir)
|
||||
return j(handler, {'ok': True, **result})
|
||||
except ValueError as e:
|
||||
return j(handler, {'error': str(e)}, status=400)
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""PR #2520: archive extraction respects HERMES_WEBUI_ATTACHMENT_DIR.
|
||||
|
||||
Verifies that extract_archive() lands files in the per-session attachment
|
||||
inbox when HERMES_WEBUI_ATTACHMENT_DIR is set, matching the single-file
|
||||
upload path and ensuring session cleanup covers extracted archives.
|
||||
"""
|
||||
import io
|
||||
import shutil
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from api.upload import extract_archive, _session_attachment_dir
|
||||
|
||||
|
||||
def _make_zip(members: dict[str, bytes]) -> bytes:
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, "w") as zf:
|
||||
for name, data in members.items():
|
||||
zf.writestr(name, data)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
class TestExtractArchiveAttachmentDir:
|
||||
|
||||
def test_extraction_lands_in_session_dir(self, tmp_path, monkeypatch):
|
||||
inbox = tmp_path / "att-inbox"
|
||||
monkeypatch.setenv("HERMES_WEBUI_ATTACHMENT_DIR", str(inbox))
|
||||
|
||||
session_id = "sess-42"
|
||||
session_dir = _session_attachment_dir(session_id)
|
||||
session_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
zip_bytes = _make_zip({
|
||||
"hello.txt": b"Hello, world!",
|
||||
"sub/nested.txt": b"Nested file",
|
||||
})
|
||||
|
||||
result = extract_archive(zip_bytes, "demo.zip", session_dir)
|
||||
|
||||
assert result["extracted"] == 2
|
||||
dest = Path(result["dest"])
|
||||
assert dest.is_relative_to(session_dir)
|
||||
assert dest.name == "demo"
|
||||
assert (dest / "hello.txt").read_text() == "Hello, world!"
|
||||
assert (dest / "sub" / "nested.txt").read_text() == "Nested file"
|
||||
|
||||
def test_session_cleanup_covers_extracted_archives(self, tmp_path, monkeypatch):
|
||||
inbox = tmp_path / "att-inbox"
|
||||
monkeypatch.setenv("HERMES_WEBUI_ATTACHMENT_DIR", str(inbox))
|
||||
|
||||
session_id = "sess-cleanup"
|
||||
session_dir = _session_attachment_dir(session_id)
|
||||
session_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
zip_bytes = _make_zip({"a.txt": b"data"})
|
||||
result = extract_archive(zip_bytes, "pkg.zip", session_dir)
|
||||
dest = Path(result["dest"])
|
||||
assert dest.exists()
|
||||
|
||||
shutil.rmtree(session_dir, ignore_errors=True)
|
||||
assert not dest.exists()
|
||||
assert not session_dir.exists()
|
||||
|
||||
def test_extraction_not_at_bare_attachment_root(self, tmp_path, monkeypatch):
|
||||
inbox = tmp_path / "att-inbox"
|
||||
monkeypatch.setenv("HERMES_WEBUI_ATTACHMENT_DIR", str(inbox))
|
||||
|
||||
session_id = "sess-scoped"
|
||||
session_dir = _session_attachment_dir(session_id)
|
||||
session_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
zip_bytes = _make_zip({"file.txt": b"content"})
|
||||
result = extract_archive(zip_bytes, "archive.zip", session_dir)
|
||||
dest = Path(result["dest"])
|
||||
|
||||
assert dest.parent == session_dir
|
||||
assert dest.parent != inbox.resolve()
|
||||
|
||||
def test_relative_files_are_relative_to_session_dir(self, tmp_path, monkeypatch):
|
||||
inbox = tmp_path / "att-inbox"
|
||||
monkeypatch.setenv("HERMES_WEBUI_ATTACHMENT_DIR", str(inbox))
|
||||
|
||||
session_dir = _session_attachment_dir("sess-rel")
|
||||
session_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
zip_bytes = _make_zip({"doc.md": b"# Title"})
|
||||
result = extract_archive(zip_bytes, "docs.zip", session_dir)
|
||||
|
||||
assert len(result["files"]) == 1
|
||||
rel = result["files"][0]
|
||||
assert rel == "docs/doc.md"
|
||||
assert (session_dir / rel).exists()
|
||||
Reference in New Issue
Block a user