6d875662fa
Walks the downloads tree and reports files that fail magic-byte validation — the "how many pre-existing files in my library are silently truncated?" question, answered. Skips the _quarantine subtree (already known-bad) and caps suspect_paths at 500 with a truncated flag so the persisted report can't grow unbounded. On-demand only: a full filesystem walk on a NAS-mounted library is expensive and the user should choose when to pay for it. Pre-existing files are reported, not quarantined — they may be the only copy and the user decides what to do. Adds GET /api/settings/library-validation (most recent report) and POST /api/settings/library-validation/run (queue a fresh sweep). Cached under cache.library_validation_report. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
82 lines
2.5 KiB
Python
82 lines
2.5 KiB
Python
"""Tests for the library-validation sweep.
|
|
|
|
Covers: scanning a fake library, ignoring the _quarantine subtree,
|
|
counting suspect files by format, and capping suspect_paths.
|
|
"""
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from app.services.file_validator import (
|
|
GIF_HEAD_89A,
|
|
JPEG_HEAD,
|
|
JPEG_TAIL,
|
|
PNG_HEAD,
|
|
PNG_TAIL,
|
|
)
|
|
from app.tasks.maintenance import scan_library_for_corruption
|
|
|
|
|
|
def _good_jpeg(p: Path) -> None:
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
p.write_bytes(JPEG_HEAD + b"\x00" * 32 + JPEG_TAIL)
|
|
|
|
|
|
def _bad_jpeg(p: Path) -> None:
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
p.write_bytes(JPEG_HEAD + b"\x00" * 32) # no EOI
|
|
|
|
|
|
def _bad_png(p: Path) -> None:
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
p.write_bytes(PNG_HEAD + b"\x00" * 32) # no IEND
|
|
|
|
|
|
def test_scan_finds_truncated_files(tmp_path):
|
|
_good_jpeg(tmp_path / "Artist" / "patreon" / "01.jpg")
|
|
_bad_jpeg(tmp_path / "Artist" / "patreon" / "02.jpg")
|
|
_bad_png(tmp_path / "Artist" / "patreon" / "03.png")
|
|
# JSON sidecars must not be scanned
|
|
(tmp_path / "Artist" / "patreon" / "01.json").write_bytes(b"{}")
|
|
|
|
report = scan_library_for_corruption(tmp_path)
|
|
|
|
assert report["scanned"] == 3
|
|
assert report["suspect_count"] == 2
|
|
assert report["by_format"] == {"jpeg": 1, "png": 1}
|
|
paths = {entry["path"] for entry in report["suspect_paths"]}
|
|
assert any("02.jpg" in p for p in paths)
|
|
assert any("03.png" in p for p in paths)
|
|
assert not report["truncated"]
|
|
|
|
|
|
def test_scan_skips_quarantine_subtree(tmp_path):
|
|
"""Files already moved to _quarantine are known-bad; don't re-scan them."""
|
|
_bad_jpeg(tmp_path / "_quarantine" / "Artist" / "patreon" / "01.jpg")
|
|
_good_jpeg(tmp_path / "Artist" / "patreon" / "02.jpg")
|
|
|
|
report = scan_library_for_corruption(tmp_path)
|
|
|
|
assert report["scanned"] == 1
|
|
assert report["suspect_count"] == 0
|
|
|
|
|
|
def test_scan_caps_suspect_paths(tmp_path):
|
|
"""suspect_count is the true total; suspect_paths is capped + truncated flag set."""
|
|
for i in range(7):
|
|
_bad_jpeg(tmp_path / "Artist" / "patreon" / f"{i:02d}.jpg")
|
|
|
|
report = scan_library_for_corruption(tmp_path, max_suspect_paths=3)
|
|
|
|
assert report["scanned"] == 7
|
|
assert report["suspect_count"] == 7
|
|
assert len(report["suspect_paths"]) == 3
|
|
assert report["truncated"] is True
|
|
|
|
|
|
def test_scan_missing_root_returns_empty_report(tmp_path):
|
|
report = scan_library_for_corruption(tmp_path / "does_not_exist")
|
|
assert report["scanned"] == 0
|
|
assert report["suspect_count"] == 0
|
|
assert report["suspect_paths"] == []
|