Complete photo deletion on disk, snappy lightbox, keyboard shortcuts, prefetching, and perfected documentation
This commit is contained in:
121
server.py
121
server.py
@@ -242,6 +242,101 @@ def journal_write(rec):
|
||||
with open(JOURNAL, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
||||
|
||||
def trash_or_remove_file(path):
|
||||
if not os.path.exists(path):
|
||||
return True
|
||||
try:
|
||||
# Try macOS Trash via AppleScript
|
||||
cmd = ["osascript", "-e", f'tell application "Finder" to delete POSIX file "{os.path.abspath(path)}"']
|
||||
r = subprocess.run(cmd, capture_output=True, timeout=10)
|
||||
if r.returncode == 0:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
os.remove(path)
|
||||
return True
|
||||
except Exception as e:
|
||||
log("error", f"Failed to delete file {path}: {e}")
|
||||
return False
|
||||
|
||||
def remove_photo_records(path):
|
||||
abs_path = os.path.abspath(path)
|
||||
# 1. Remove symlink in _organized
|
||||
with S.lock:
|
||||
folder = S.folder
|
||||
org_root = os.path.join(folder, "_organized")
|
||||
if os.path.exists(org_root):
|
||||
for root, dirs, files in os.walk(org_root):
|
||||
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
||||
for name in files:
|
||||
p = os.path.join(root, name)
|
||||
if os.path.islink(p):
|
||||
try:
|
||||
if os.path.realpath(p) == abs_path:
|
||||
os.remove(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. Clean thumbnail and view caches
|
||||
try:
|
||||
h = hashlib.md5(abs_path.encode()).hexdigest()
|
||||
t1 = os.path.join(tempfile.gettempdir(), "photon_thumbs", f"{h}.jpg")
|
||||
t2 = os.path.join(tempfile.gettempdir(), "photon_view", f"{h}.jpg")
|
||||
for t in (t1, t2):
|
||||
if os.path.exists(t):
|
||||
try: os.remove(t)
|
||||
except Exception: pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 3. Rewrite journal without this path
|
||||
if os.path.exists(JOURNAL):
|
||||
lines_to_keep = []
|
||||
with open(JOURNAL, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line_str = line.strip()
|
||||
if not line_str:
|
||||
continue
|
||||
try:
|
||||
rec = json.loads(line_str)
|
||||
if os.path.abspath(rec.get("path", "")) != abs_path:
|
||||
lines_to_keep.append(line_str)
|
||||
except Exception:
|
||||
lines_to_keep.append(line_str)
|
||||
with open(JOURNAL, "w", encoding="utf-8") as f:
|
||||
for l in lines_to_keep:
|
||||
f.write(l + "\n")
|
||||
|
||||
# 4. Remove from State
|
||||
with S.lock:
|
||||
if abs_path in S.done_paths:
|
||||
S.done_paths.remove(abs_path)
|
||||
if abs_path in S.files:
|
||||
S.files.remove(abs_path)
|
||||
if abs_path in S.video_files:
|
||||
S.video_files.remove(abs_path)
|
||||
if abs_path in S.failed_paths:
|
||||
S.failed_paths.remove(abs_path)
|
||||
write_failures()
|
||||
|
||||
global SEARCH_CACHE
|
||||
SEARCH_CACHE = {"mtime": None, "records": []}
|
||||
load_journal()
|
||||
push_stats()
|
||||
|
||||
def delete_photo_file_and_record(path):
|
||||
if not path:
|
||||
return False, "path is required"
|
||||
abs_path = os.path.abspath(path)
|
||||
deleted_disk = False
|
||||
if os.path.exists(abs_path):
|
||||
deleted_disk = trash_or_remove_file(abs_path)
|
||||
else:
|
||||
deleted_disk = True
|
||||
remove_photo_records(abs_path)
|
||||
return deleted_disk, None
|
||||
|
||||
SEARCH_CACHE = {"mtime": None, "records": []}
|
||||
|
||||
def load_search_index():
|
||||
@@ -1864,6 +1959,32 @@ class Handler(BaseHTTPRequestHandler):
|
||||
except Exception as e:
|
||||
self._json({"error": str(e)}, 500)
|
||||
|
||||
elif self.path == "/api/delete_photo":
|
||||
path = body.get("path")
|
||||
if not path:
|
||||
self._json({"error": "path is required"}, 400); return
|
||||
success, err = delete_photo_file_and_record(path)
|
||||
if success:
|
||||
log("ok", f"Deleted photo from disk: {os.path.basename(path)}")
|
||||
self._json({"ok": True, "path": path})
|
||||
else:
|
||||
self._json({"error": f"Failed deleting file: {err}"}, 500)
|
||||
|
||||
elif self.path == "/api/delete_photos":
|
||||
paths = body.get("paths") or []
|
||||
if not paths:
|
||||
self._json({"error": "paths array is required"}, 400); return
|
||||
deleted_count = 0
|
||||
failed = []
|
||||
for p in paths:
|
||||
success, err = delete_photo_file_and_record(p)
|
||||
if success:
|
||||
deleted_count += 1
|
||||
else:
|
||||
failed.append({"path": p, "error": err})
|
||||
log("ok", f"Batch deleted {deleted_count} photos from disk" + (f" ({len(failed)} failed)" if failed else ""))
|
||||
self._json({"ok": True, "deleted": deleted_count, "failed": failed})
|
||||
|
||||
elif self.path == "/api/dedupe/build":
|
||||
with S.lock:
|
||||
already_running = S.dedupe_status == "running"
|
||||
|
||||
Reference in New Issue
Block a user