fix(nc-webhook): propagate folder deletes + resurrect un-discarded files

Two bugs surfaced by the Phase 2 deletion-roundtrip test:

A) Folder delete in NC only fires one NodeDeletedEvent (for the folder
   itself, no .jpg suffix). The handler bailed with "unsupported
   extension" and photos inside the folder kept is_discarded=false in
   mule until the 30-min discard_missing_photos_beat caught up.

   Fix: when the deleted path has no supported extension, call new
   `handle_directory_deletion()` which UPDATEs every Photo whose
   filepath starts with `dirpath + '/'`. Single SQL statement,
   idempotent (excludes already-discarded rows so re-deliveries don't
   re-stamp discarded_at).

C) PUT-overwrite of a previously-discarded file fired NodeWrittenEvent
   → scan_folder, but scan_folder's "Photo exists by filepath, skip"
   branch left is_discarded=true. File was back on disk; mule still
   treated it as gone.

   Fix: in that branch, if the existing row is discarded, flip
   is_discarded=false + clear discarded_at + re-queue extract_metadata
   so EXIF / nextcloud_fileid pick up any changes to the bytes.

Together these close the gap for "delete then put back" round-trips
via the NC webhook path. Trashbin-restore (bug B in the test report)
remains an NC-side gap — NC doesn't emit any event mule subscribes to
for restore-from-trash. That stays a TODO.
This commit is contained in:
Claudio
2026-05-11 12:50:52 +02:00
parent f657e2c0ba
commit 94088253f8
2 changed files with 68 additions and 4 deletions

View File

@@ -126,7 +126,11 @@ async def nc_webhook(
# Import lazily so this router can load before the celery app is
# ready — important when the backend boots before broker is up.
from app.tasks.scan import scan_folder, handle_file_deletion
from app.tasks.scan import (
scan_folder,
handle_file_deletion,
handle_directory_deletion,
)
supported = _supported_extensions()
@@ -154,8 +158,22 @@ async def nc_webhook(
abs_path = _nc_path_to_abs(nc_path) if nc_path else None
if not abs_path:
return {"status": "ignored", "reason": "non-user-files path"}
# Folder deletes: NC fires exactly one NodeDeletedEvent for the
# folder, not one per child file. Detect the directory case by
# the absence of a supported image extension and recursively
# discard every Photo under that prefix.
if Path(abs_path).suffix.lower() not in supported:
return {"status": "ignored", "reason": "unsupported extension"}
n = await handle_directory_deletion(abs_path)
logger.info(
"nc-webhook deleted (dir): %s -> %s photos discarded",
abs_path, n,
)
return {
"status": "applied",
"action": "discard_subtree",
"path": abs_path,
"discarded": n,
}
await handle_file_deletion(abs_path)
logger.info("nc-webhook deleted: marked %s as discarded", abs_path)
return {"status": "applied", "action": "discard", "path": abs_path}