From e2345c659dbd645651f1bb827b8ced664c6b6759 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:04:11 -0700 Subject: [PATCH] Add mass orphan safety guard to prevent accidental library deletion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When >50% of files are flagged as orphans (likely a DB path mismatch), findings are marked as warnings with mass_orphan flag. Fixing these requires typing "witness me" to confirm — prevents nuking an entire library from a false-positive orphan scan. --- core/repair_jobs/orphan_file_detector.py | 85 ++++++++++------ webui/static/script.js | 121 +++++++++++++++++++++-- 2 files changed, 167 insertions(+), 39 deletions(-) diff --git a/core/repair_jobs/orphan_file_detector.py b/core/repair_jobs/orphan_file_detector.py index 771a2842..5f2de742 100644 --- a/core/repair_jobs/orphan_file_detector.py +++ b/core/repair_jobs/orphan_file_detector.py @@ -81,6 +81,8 @@ class OrphanFileDetectorJob(RepairJob): if context.report_progress: context.report_progress(phase=f'Checking {total} files...', total=total) + orphan_files = [] + for i, fpath in enumerate(audio_files): if context.check_stop(): return result @@ -107,41 +109,62 @@ class OrphanFileDetectorJob(RepairJob): break if not is_known: - # This file is an orphan — create finding - if context.report_progress: - context.report_progress( - log_line=f'Orphan: {os.path.basename(fpath)}', - log_type='skip' - ) - try: - stat = os.stat(fpath) - ext = os.path.splitext(fpath)[1].lower().lstrip('.') - if context.create_finding: - context.create_finding( - job_id=self.job_id, - finding_type='orphan_file', - severity='info', - entity_type='file', - entity_id=None, - file_path=fpath, - title=f'Orphan file: {os.path.basename(fpath)}', - description=f'Audio file in transfer folder is not tracked in the database', - details={ - 'file_size': stat.st_size, - 'format': ext, - 'modified': time.strftime('%Y-%m-%d %H:%M:%S', - time.localtime(stat.st_mtime)), - 'folder': os.path.dirname(fpath), - } - ) - result.findings_created += 1 - except Exception as e: - logger.debug("Error creating orphan finding for %s: %s", fpath, e) - result.errors += 1 + orphan_files.append(fpath) if context.update_progress and (i + 1) % 50 == 0: context.update_progress(i + 1, total) + # Safety check: if most files look like orphans, it's probably a path + # mismatch between the DB and filesystem — not actual orphans. + orphan_ratio = len(orphan_files) / total if total else 0 + mass_orphan = orphan_ratio > 0.5 and len(orphan_files) > 20 + + if mass_orphan: + logger.warning( + "Mass orphan warning: %d of %d files (%.0f%%) flagged as orphans — " + "this likely indicates a DB path mismatch, not actual orphans", + len(orphan_files), total, orphan_ratio * 100 + ) + + for fpath in orphan_files: + if context.report_progress: + context.report_progress( + log_line=f'Orphan: {os.path.basename(fpath)}', + log_type='skip' + ) + try: + stat = os.stat(fpath) + ext = os.path.splitext(fpath)[1].lower().lstrip('.') + if context.create_finding: + context.create_finding( + job_id=self.job_id, + finding_type='orphan_file', + severity='warning' if mass_orphan else 'info', + entity_type='file', + entity_id=None, + file_path=fpath, + title=f'Orphan file: {os.path.basename(fpath)}', + description=( + 'Audio file in transfer folder is not tracked in the database. ' + 'WARNING: Mass orphan detection triggered — this may be a path ' + 'mismatch, not actual orphans. Verify before deleting!' + ) if mass_orphan else ( + 'Audio file in transfer folder is not tracked in the database' + ), + details={ + 'file_size': stat.st_size, + 'format': ext, + 'modified': time.strftime('%Y-%m-%d %H:%M:%S', + time.localtime(stat.st_mtime)), + 'folder': os.path.dirname(fpath), + 'mass_orphan': mass_orphan, + } + ) + result.findings_created += 1 + except Exception as e: + logger.debug("Error creating orphan finding for %s: %s", fpath, e) + result.errors += 1 + if context.update_progress: context.update_progress(total, total) diff --git a/webui/static/script.js b/webui/static/script.js index 803abeeb..ce33955f 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -172,6 +172,92 @@ function resolveConfirmDialog(result) { } } +/** + * Nuclear confirmation dialog for mass-destructive operations. + * User must type an exact phrase to proceed. + */ +function showWitnessMeDialog(orphanCount) { + return new Promise(resolve => { + const overlay = document.createElement('div'); + overlay.className = 'confirm-modal-overlay'; + overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:10000;display:flex;align-items:center;justify-content:center;'; + + overlay.innerHTML = ` +
+ You are about to permanently delete ${orphanCount.toLocaleString()} files from your disk. +
++ This many orphans usually means a path mismatch between your database and filesystem + — not actual orphan files. A previous user lost their entire library this way. +
++ To confirm you understand the risk, type witness me below: +
+ +