FileViewer's select mode (FileViewer-React #72 / PR #92) tags many photos at once. Today that is one POST /image/tags per photo × tag, and reading the tag write path turned up several bugs.
Bugs (confirmed against SQLite)
Adding a tag a photo already has → 500.tag_file is a plain INSERT and tagged_photo has UNIQUE(rel_path, tag_id). A retry, or a bulk tag over a selection where some photos already carry it, fails.
Re-adding a tag that has no photos → 500.add_tag looks the name up via get_all_tags, which inner-joins tagged_photo, so an unused tag isn't found; create_tag then hits idx_tags_name_nocase. Same lookup in update_tags silently skips unused tag ids.
Removing a tag can answer 200 and remove nothing. Reads union tags over every path sharing the photo's content_hash; remove_tag deletes only the exact (canonical) path.
Removal matches the name case-sensitively, while add matches case-insensitively.
POST /image/tags/batch panics while holding the TagDao lock (panic! / unwrap), poisoning it so every later tag request panics until restart.
New endpoint
POST /image/tags/bulk
{ "file_names": [...], "add": ["travel", "Road trip"], "remove": [12, 40] }
-> { "added": n, "removed": n, "tags": [Tag...] } // tags = the resolved `add` tags
One transaction: all of it applies or none.
add by name, case-insensitive, created when missing; already-tagged = no-op.
remove by id, across every path sharing the photo's content (and a version's original), matching reads.
400 when both lists are empty, file_names is empty or too long, or a tag is in both.
No schema change.
FileViewer's select mode (FileViewer-React #72 / PR #92) tags many photos at once. Today that is one `POST /image/tags` per photo × tag, and reading the tag write path turned up several bugs.
## Bugs (confirmed against SQLite)
1. **Adding a tag a photo already has → 500.** `tag_file` is a plain INSERT and `tagged_photo` has `UNIQUE(rel_path, tag_id)`. A retry, or a bulk tag over a selection where some photos already carry it, fails.
2. **Re-adding a tag that has no photos → 500.** `add_tag` looks the name up via `get_all_tags`, which inner-joins `tagged_photo`, so an unused tag isn't found; `create_tag` then hits `idx_tags_name_nocase`. Same lookup in `update_tags` silently skips unused tag ids.
3. **Removing a tag can answer 200 and remove nothing.** Reads union tags over every path sharing the photo's `content_hash`; `remove_tag` deletes only the exact (canonical) path.
4. **Removal matches the name case-sensitively**, while add matches case-insensitively.
5. **`POST /image/tags/batch` panics while holding the TagDao lock** (`panic!` / `unwrap`), poisoning it so every later tag request panics until restart.
## New endpoint
```
POST /image/tags/bulk
{ "file_names": [...], "add": ["travel", "Road trip"], "remove": [12, 40] }
-> { "added": n, "removed": n, "tags": [Tag...] } // tags = the resolved `add` tags
```
- One transaction: all of it applies or none.
- `add` by name, case-insensitive, created when missing; already-tagged = no-op.
- `remove` by id, across every path sharing the photo's content (and a version's original), matching reads.
- 400 when both lists are empty, file_names is empty or too long, or a tag is in both.
No schema change.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
FileViewer's select mode (FileViewer-React #72 / PR #92) tags many photos at once. Today that is one
POST /image/tagsper photo × tag, and reading the tag write path turned up several bugs.Bugs (confirmed against SQLite)
tag_fileis a plain INSERT andtagged_photohasUNIQUE(rel_path, tag_id). A retry, or a bulk tag over a selection where some photos already carry it, fails.add_taglooks the name up viaget_all_tags, which inner-joinstagged_photo, so an unused tag isn't found;create_tagthen hitsidx_tags_name_nocase. Same lookup inupdate_tagssilently skips unused tag ids.content_hash;remove_tagdeletes only the exact (canonical) path.POST /image/tags/batchpanics while holding the TagDao lock (panic!/unwrap), poisoning it so every later tag request panics until restart.New endpoint
addby name, case-insensitive, created when missing; already-tagged = no-op.removeby id, across every path sharing the photo's content (and a version's original), matching reads.No schema change.