Tag writes: POST /image/tags/bulk, and fix remove/re-add/duplicate bugs

- POST /image/tags/bulk { file_names, add: [names], remove: [ids] }: one
  transaction over many photos; names match case-insensitively and are
  created when missing; 400 if a tag is both added and removed.
- Adding a tag a photo already has is a no-op (was a 500 on
  UNIQUE(rel_path, tag_id)).
- Adding a tag no photo carries reuses it (the lookup only saw tags in
  use, so create_tag hit idx_tags_name_nocase: 500). update_tags could
  likewise skip such ids silently.
- Removing a tag matches the name case-insensitively and deletes it from
  every path sharing the photo's content, as reads union them; it used to
  answer 200 and leave the tag on the other copy.
- update_tags returns errors instead of panicking with the TagDao lock
  held, which poisoned it for every later tag request.

Closes #200

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cameron CordesandClaude Opus 5.5 committed 2026-11-01 12:49:31 -05:00
1 parent 9f013d7b78
commit 96a2858a50
2 files changed
+673 -131

No files matched your search

+5 -3
View File
@@ -727,9 +727,11 @@ GET /video/original/stream?token=... (Range-capable original bytes)
// Tags
GET /image/tags/all (tags + counts, camera_makes, and cameras: make+model bodies)
POST /image/tags (add tag to file)
DELETE /image/tags (remove tag from file)
POST /image/tags/batch (bulk tag updates)
POST /image/tags (add tag to file; by name, case-insensitive, created when missing; already tagged = no-op)
DELETE /image/tags (remove tag from file: every path sharing its content, matching reads)
POST /image/tags/batch (replace ONE file's tag set: { file_name, tag_ids })
POST /image/tags/bulk (many files, one transaction: { file_names, add: [names], remove: [ids] }
-> { added, removed, tags }; 400 if a tag is in both)
// Photo versions (#191): AI restorations / upscales / edits shown as ONE photo
GET /image/versions?path=...&library=... (path of the original or any version ->