mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
fix(archive): never delete persistent files in 3MF cache cleanup (#1212)
Daily builds since 889c8bd8 (Apr 29) silently destroyed archive copies
and library file bytes on every print completion. Reprint / View G-code
later returned 404 with no log line explaining why; the DB row was
intact and the archive grid kept showing the entry, but the file
behind archive.file_path no longer existed on disk.
Root cause: #1166 added three dispatch sites that cache the live
archive copy (and library file bytes for Direct-Print) in the shared
3MF download cache, so /cover could skip a redundant FTP transfer
mid-print. The cache was originally designed for transient downloads
under archive_dir/temp/, and clear_3mf_cache(printer_id) — called
from on_print_complete to keep that temp dir from accumulating —
happily unlink()'d every cached path. Path.exists() guarded the
unlink, so no exception, no warning, just silent destruction. Listing
didn't change; only acting on the archive surfaced the 404.
Fix: clear_3mf_cache._maybe_unlink refuses to unlink any path outside
archive_dir/temp. Cache dict is still cleared (so re-cache continues
to work and /cover hits a fresh path next print), only the on-disk
delete is gated. Persistent locations — archive/<printer_id>/...,
archive/unassigned/... (VP-archived prints with printer_id=None),
library_files/..., is_external library mounts — all survive.
Regression test test_clear_does_not_delete_persistent_files pins the
contract end-to-end: archive 3mf, library 3mf, and temp 3mf all
cached for the same printer; after clear, all three cache entries
are dropped from the dict, but only the temp file is unlinked from
disk. Two existing tests updated to put fixtures under
archive_dir/temp.
This commit is contained in:
@@ -1198,26 +1198,78 @@ class TestThreeMFCache:
|
||||
cache_3mf_download(1, "A.3mf", f)
|
||||
assert get_cached_3mf(1, "A.3mf") == f
|
||||
|
||||
def test_clear_by_printer_scoped(self, tmp_path):
|
||||
def test_clear_by_printer_scoped(self, tmp_path, monkeypatch):
|
||||
"""Clearing one printer leaves the other untouched."""
|
||||
f1 = tmp_path / "one.3mf"
|
||||
from backend.app.core import config as _config
|
||||
|
||||
monkeypatch.setattr(_config.settings, "archive_dir", tmp_path)
|
||||
temp_dir = tmp_path / "temp"
|
||||
temp_dir.mkdir()
|
||||
f1 = temp_dir / "one.3mf"
|
||||
f1.write_bytes(b"1")
|
||||
f2 = tmp_path / "two.3mf"
|
||||
f2 = temp_dir / "two.3mf"
|
||||
f2.write_bytes(b"2")
|
||||
cache_3mf_download(1, "one.3mf", f1)
|
||||
cache_3mf_download(2, "two.3mf", f2)
|
||||
clear_3mf_cache(1)
|
||||
assert get_cached_3mf(1, "one.3mf") is None
|
||||
assert get_cached_3mf(2, "two.3mf") == f2
|
||||
# clear_3mf_cache defaulted to delete_files=True, so the file is gone
|
||||
# clear_3mf_cache defaulted to delete_files=True, so the temp file is gone
|
||||
assert not f1.exists()
|
||||
assert f2.exists()
|
||||
|
||||
def test_clear_without_deleting_files(self, tmp_path):
|
||||
def test_clear_without_deleting_files(self, tmp_path, monkeypatch):
|
||||
"""delete_files=False leaves files on disk — used by tests."""
|
||||
f = tmp_path / "keep.3mf"
|
||||
from backend.app.core import config as _config
|
||||
|
||||
monkeypatch.setattr(_config.settings, "archive_dir", tmp_path)
|
||||
temp_dir = tmp_path / "temp"
|
||||
temp_dir.mkdir()
|
||||
f = temp_dir / "keep.3mf"
|
||||
f.write_bytes(b"x")
|
||||
cache_3mf_download(1, "keep.3mf", f)
|
||||
clear_3mf_cache(1, delete_files=False)
|
||||
assert get_cached_3mf(1, "keep.3mf") is None
|
||||
assert f.exists()
|
||||
|
||||
def test_clear_does_not_delete_persistent_files(self, tmp_path, monkeypatch):
|
||||
"""Regression for #1212 / "file disappeared overnight" reports.
|
||||
|
||||
Dispatch sites added in #1166 cache the live archive copy and library
|
||||
file bytes — paths outside ``archive_dir/temp`` — so /cover can skip
|
||||
FTP. Those files are user data; the cache cleanup must never unlink
|
||||
them. Pre-fix, ``clear_3mf_cache(printer_id, delete_files=True)`` ran
|
||||
on every ``on_print_complete`` and silently destroyed them, leaving a
|
||||
DB row whose ``file_path`` pointed at nothing — breaking Reprint and
|
||||
View G-code with a 404.
|
||||
"""
|
||||
from backend.app.core import config as _config
|
||||
|
||||
monkeypatch.setattr(_config.settings, "archive_dir", tmp_path / "archive")
|
||||
(tmp_path / "archive" / "temp").mkdir(parents=True)
|
||||
|
||||
archive_file = tmp_path / "archive" / "1" / "20260504_wallhooks" / "wallhooks.gcode.3mf"
|
||||
archive_file.parent.mkdir(parents=True)
|
||||
archive_file.write_bytes(b"archive bytes")
|
||||
|
||||
library_file = tmp_path / "library_files" / "abcd.3mf"
|
||||
library_file.parent.mkdir(parents=True)
|
||||
library_file.write_bytes(b"library bytes")
|
||||
|
||||
temp_file = tmp_path / "archive" / "temp" / "cover_1_x.3mf"
|
||||
temp_file.write_bytes(b"temp bytes")
|
||||
|
||||
cache_3mf_download(1, "wallhooks.gcode.3mf", archive_file)
|
||||
cache_3mf_download(1, "library.3mf", library_file)
|
||||
cache_3mf_download(1, "cover_1_x.3mf", temp_file)
|
||||
|
||||
clear_3mf_cache(1)
|
||||
|
||||
# All three cache entries are dropped from the dict.
|
||||
assert get_cached_3mf(1, "wallhooks.gcode.3mf") is None
|
||||
assert get_cached_3mf(1, "library.3mf") is None
|
||||
assert get_cached_3mf(1, "cover_1_x.3mf") is None
|
||||
# But only the temp file is unlinked — user data survives.
|
||||
assert archive_file.exists(), "archive 3mf must not be deleted by cache cleanup"
|
||||
assert library_file.exists(), "library 3mf must not be deleted by cache cleanup"
|
||||
assert not temp_file.exists(), "temp file should still be cleaned up"
|
||||
|
||||
Reference in New Issue
Block a user