From 85f7a188834e8e63c4f8fa6b211e61a1461515ea Mon Sep 17 00:00:00 2001 From: wander Date: Sun, 8 Mar 2026 05:10:37 -0400 Subject: [PATCH] feat: add filesystem permissions check and improved batch deletion error handling --- ta_symlink.py | 94 +++++++------- ui/src/components/RecoveryModal.svelte | 172 ++++++++++++++++++------- 2 files changed, 170 insertions(+), 96 deletions(-) diff --git a/ta_symlink.py b/ta_symlink.py index a6ded67..633d70e 100644 --- a/ta_symlink.py +++ b/ta_symlink.py @@ -1139,62 +1139,60 @@ def api_recovery_start(): @app.route("/api/recovery/delete-batch", methods=["POST"]) @requires_auth def api_recovery_delete_batch(): - data = request.get_json() - filepaths = data.get('filepaths', []) - destruct_mode = data.get('destruct_mode', False) + data = request.json + paths = data.get("filepaths", []) + destruct = data.get("destruct_mode", False) - if not filepaths: - return jsonify({"error": "No filepaths provided"}), 400 - - results = [] - # Refresh metadata to ensure we have latest paths for destruct mode - video_map = fetch_all_metadata() if destruct_mode else {} + success_count = 0 + fail_count = 0 + errors = [] - for filepath in filepaths: - p = Path(filepath) - if not p.exists(): - results.append({"path": filepath, "success": False, "error": "File not found"}) - continue - + # Refresh metadata for destruct mode + video_map = fetch_all_metadata() if destruct else {} + + for path in paths: try: - vid_id = extract_id_from_filename(p.name) - - # DESTRUCT MODE: Delete source too - if destruct_mode and vid_id: - meta = video_map.get(vid_id) - if meta and meta.get('filesystem_path'): - source_path = Path(meta['filesystem_path']) - if source_path.exists(): - source_path.unlink() - log(f" [DESTRUCT] Deleted source: {source_path}") - - # Also check lost_media table - with get_db() as conn: - conn.execute("DELETE FROM lost_media WHERE video_id = ?", (vid_id,)) - conn.commit() + # 1. Destruct Source if enabled + if destruct: + source_deleted = False + for vid_id, meta in video_map.items(): + if meta.get('path') == path or meta.get('filesystem_path') == path: + source_path = meta.get('filesystem_path') + if source_path and os.path.exists(source_path): + os.remove(source_path) + log(f"☢️ [DESTRUCT] Deleted source: {source_path}") + source_deleted = True + break + if not source_deleted: + log(f"⚠️ [DESTRUCT] Source not found for: {path}") - # DELETE TARGET (Symlink/Resource) - p.unlink() - - # Clean up empty parent folder if it's a video folder - parent = p.parent - if parent not in [TARGET_DIR, HIDDEN_DIR, SOURCE_DIR] and parent.name != "source": - try: - if not any(parent.iterdir()): + # 2. Delete Target + p = Path(path) + if p.exists(): + if p.is_dir(): + shutil.rmtree(p) + else: + p.unlink() + + # 3. Cleanup empty parent + parent = p.parent + if parent != Path(TARGET_DIR) and parent != Path(HIDDEN_DIR): + if parent.exists() and not any(parent.iterdir()): parent.rmdir() - log(f" [CLEANUP] Removed empty dir: {parent}") - except: - pass - - results.append({"path": filepath, "success": True}) + log(f"🧹 [CLEANUP] Removed empty folder: {parent}") + + success_count += 1 except Exception as e: - results.append({"path": filepath, "success": False, "error": str(e)}) - log(f"❌ Failed to delete {filepath}: {e}") + err_msg = str(e) + log(f"❌ Failed to delete {path}: {err_msg}") + fail_count += 1 + if err_msg not in errors: + errors.append(err_msg) return jsonify({ - "results": results, - "success_count": len([r for r in results if r["success"]]), - "fail_count": len([r for r in results if not r["success"]]) + "success_count": success_count, + "fail_count": fail_count, + "errors": errors[:5] }) @app.route("/api/recovery/delete", methods=["POST"]) diff --git a/ui/src/components/RecoveryModal.svelte b/ui/src/components/RecoveryModal.svelte index 684fbd0..63ad641 100644 --- a/ui/src/components/RecoveryModal.svelte +++ b/ui/src/components/RecoveryModal.svelte @@ -8,7 +8,7 @@ let results: any = { unindexed: [], rescue: [], redundant: [], lost: [] }; let pollInterval: ReturnType; - // New State for Destruct Mode & Multi-selection + // State for Destruct Mode & Multi-selection let destructMode = false; let selectedPaths = new Set(); @@ -19,6 +19,22 @@ let loadingChannels = false; let searchingVideos = false; + // Permissions state + let permissions: any = null; + let loadingPermissions = false; + + async function checkPermissions() { + loadingPermissions = true; + try { + const res = await fetch("/api/system/check-permissions"); + permissions = await res.json(); + } catch (e) { + console.error("Failed to check permissions", e); + } finally { + loadingPermissions = false; + } + } + async function startScan() { scanning = true; try { @@ -111,7 +127,6 @@ } if (!confirm(msg)) return; - if (destructMode && !confirm("FINAL WARNING: This is IRREVERSIBLE. Delete source file now?")) return; try { @@ -128,7 +143,10 @@ alert("Deleted."); if (activeTab === "channels") fetchChannelVideos(selectedChannel); else startScan(); - } else alert("Error deleting file."); + } else { + const err = d.errors?.[0] || "Unknown error"; + alert(`Error deleting file: ${err}`); + } } catch (e) { alert(e); } @@ -155,7 +173,14 @@ headers: { "Content-Type": "application/json" }, }); const d = await res.json(); - alert(`Batch complete. Success: ${d.success_count}, Failed: ${d.fail_count}`); + + if (d.fail_count > 0) { + const firstErr = d.errors?.[0] || "Check backend logs."; + alert(`Batch partially failed.\nSuccess: ${d.success_count}\nFailed: ${d.fail_count}\n\nFirst error: ${firstErr}`); + } else { + alert(`Batch complete. Deleted ${d.success_count} items.`); + } + selectedPaths = new Set(); if (activeTab === "channels") fetchChannelVideos(selectedChannel); else startScan(); @@ -170,7 +195,7 @@ } else { selectedPaths.add(path); } - selectedPaths = selectedPaths; // Trigger Svelte reactivity + selectedPaths = selectedPaths; } function toggleAll() { @@ -184,8 +209,12 @@ $: allSelected = (activeTab === "channels" ? channelVideos : results[activeTab])?.length > 0 && selectedPaths.size === (activeTab === "channels" ? channelVideos : results[activeTab])?.length; + $: sourceRO = permissions?.source?.writeable === false; + $: targetRO = permissions?.target?.writeable === false; + onMount(() => { fetchChannels(); + checkPermissions(); }); @@ -193,11 +222,11 @@ class="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm p-4" >

Advanced Recovery @@ -207,16 +236,36 @@

+ + {#if sourceRO || targetRO} +
+ +
+ Filesystem Alert: + {#if sourceRO && targetRO} + Both Source AND Target directories are currently READ-ONLY. Deletion and reorganization will fail. + {:else if sourceRO} + Source archive is READ-ONLY. Destruct Mode will fail. + {:else} + Target library is READ-ONLY. Symlink cleanup will fail. + {/if} +
+ +
+ {/if} +
@@ -238,29 +288,31 @@ -
Status: {status}
+
Status: {status}
-
+
{#each ["unindexed", "rescue", "redundant", "lost", "channels"] as tab}
-
+
{#if searchingVideos} -
- Fetching videos... +
+
+ QUERYING TA DATABASE...
{:else if channelVideos.length > 0} - - +
+ @@ -344,9 +399,9 @@
{item.title}
{:else} -
- - Select a channel to view its videos. +
+ + No channel selected
{/if}
@@ -356,11 +411,11 @@ class="flex flex-col items-center justify-center h-full text-neon-cyan gap-4" >
-
SYSTEM WIDE SCAN IN PROGRESS...
+
SYSTEM WIDE SCAN IN PROGRESS...
{:else} - - +
+ - - - + + + @@ -408,33 +463,35 @@
{#if activeTab === "unindexed"} RECOVER {:else if activeTab === "redundant"} DELETE {:else if activeTab === "lost"} FORCE DELETE {:else} RESCUE {/if}
@@ -445,10 +502,10 @@ @@ -464,4 +521,23 @@ .accent-neon-cyan { accent-color: #00f3ff; } + + .bg-cyber-card { + background: linear-gradient(135deg, #0f0f13 0%, #050505 100%); + } + + /* Custom Scrollbar for better Cyberpunk feel */ + ::-webkit-scrollbar { + width: 4px; + } + ::-webkit-scrollbar-track { + background: rgba(15, 15, 15, 0.5); + } + ::-webkit-scrollbar-thumb { + background: #00f3ff22; + border-radius: 10px; + } + ::-webkit-scrollbar-thumb:hover { + background: #00f3ff66; + }
Video IDFilename / PathSize/InfoIDFilename / Target PathInfo Action
- - Nothing detected in this category. + + NO ANOMALIES DETECTED IN THIS SECTOR.