Commit Graph

3 Commits

Author SHA1 Message Date
Claudio
f4a03b63f4 feat(nc-webhook): handle folder rename via NodeRenamedEvent
NC fires one NodeRenamedEvent on a directory rename — children don't
get their own events. The handler bailed on both paths having no
supported extension. Now:

- New `handle_directory_rename(old, new)` in scan.py does a single
  transaction of prefix-rewrites against photos.filepath, folders.path,
  and source_roots.path. Cross-source-root case (Photos/x → Memories/x)
  is treated as discard-the-old-subtree; scan_folder dispatched by the
  subsequent NodeWritten/NodeCreated picks up the new root.

- Webhook renamed branch checks "both source and target are
  directories" and calls the helper. File renames keep the existing
  delete-old + scan-new-parent path.

Idempotent: the SQL matches zero rows the second time around. That
makes the feedback loop safe — mule's existing PATCH /folders/{id}
endpoint already does a WebDAV MOVE + inline DB rewrite for NC paths,
and the resulting NodeRenamedEvent now flows back through this handler
without re-running the rewrite or leaving rows stale.

Trashbin restore (the documented "NC doesn't emit a subscribed event"
gap) is unchanged.
2026-05-11 13:07:28 +02:00
Claudio
94088253f8 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.
2026-05-11 12:50:52 +02:00
Claudio
362fbc6d83 feat(nc-webhook): receive Nextcloud file events instead of polling
The watchfiles-based watcher works but duplicates Nextcloud's own
notion of "this file changed." NC has a webhook_listeners app that
can POST file events to an external URL. This adds the mule side of
that handshake.

- POST /api/v1/internal/nc-webhook authenticates a Bearer token
  (NEXTCLOUD_WEBHOOK_SECRET, hmac.compare_digest) and dispatches the
  same scan_folder / handle_file_deletion machinery the watcher used.
- Handles NodeCreated, NodeWritten, NodeDeleted, NodeRenamed.
  Renamed is mapped to delete-old + scan-new-parent. Maps NC's
  /admin/files/... path to the bind-mounted /nextcloud-users/admin/files/...
- backend/scripts/register_nc_webhooks.py is the idempotent
  registrar: lists existing webhooks, deletes any pointing at the
  target URL, then POSTs four fresh ones via OCS.
- Sets the env passthrough on backend + all workers in compose so
  the same secret is available wherever the registrar might run.

watch_folders stays in place for now — webhooks become primary, the
watcher is a belt-and-suspenders fallback. Drop the watcher in a
follow-up once webhooks are proven reliable on this NC instance.
2026-05-11 12:19:59 +02:00