feat(maintenance): add library-validation sweep task and API
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>
This commit is contained in:
@@ -6,7 +6,14 @@ from quart import Blueprint, request, jsonify, current_app
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.setting import Setting, DEFAULT_SETTINGS, EXTENSION_API_KEY_SETTING, STORAGE_STATS_SETTING, generate_api_key
|
||||
from app.models.setting import (
|
||||
Setting,
|
||||
DEFAULT_SETTINGS,
|
||||
EXTENSION_API_KEY_SETTING,
|
||||
STORAGE_STATS_SETTING,
|
||||
LIBRARY_VALIDATION_REPORT_SETTING,
|
||||
generate_api_key,
|
||||
)
|
||||
from app.config import get_settings
|
||||
from app.services.gallery_dl import GalleryDLService
|
||||
|
||||
@@ -270,6 +277,32 @@ async def refresh_storage_stats():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@bp.route("/library-validation", methods=["GET"])
|
||||
async def get_library_validation_report():
|
||||
"""Return the most recent library-validation sweep report (or null)."""
|
||||
async with current_app.db_engine.connect() as conn:
|
||||
async with AsyncSession(bind=conn) as session:
|
||||
result = await session.execute(
|
||||
select(Setting).where(Setting.key == LIBRARY_VALIDATION_REPORT_SETTING)
|
||||
)
|
||||
setting = result.scalar_one_or_none()
|
||||
return jsonify(setting.value if setting and setting.value else None)
|
||||
|
||||
|
||||
@bp.route("/library-validation/run", methods=["POST"])
|
||||
async def run_library_validation():
|
||||
"""Queue a library-validation sweep (walks the library, reports truncated files)."""
|
||||
from app.tasks.maintenance import validate_library
|
||||
|
||||
try:
|
||||
validate_library.delay()
|
||||
current_app.logger.info("Queued library validation sweep")
|
||||
return jsonify({"message": "Library validation queued"})
|
||||
except Exception as e:
|
||||
current_app.logger.error(f"Failed to queue library validation: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@bp.route("/logs", methods=["GET"])
|
||||
async def get_logs():
|
||||
"""Get recent download logs from download metadata.
|
||||
|
||||
Reference in New Issue
Block a user