fix(media): a bundle delete removes only what the bundle listing shows #276

Merged
dominik.polakovics merged 1 commit from afk/269 into main 2026-10-13 22:16:14 +02:00

Closes #269

What

POST /api/media/delete with a bundle reference used to delete any file in the page's bundle directory, including the page's own _index.md, _index.<lang>.md, notes.txt and dotfiles. Now it removes a file only when GET /api/media/bundle would list it. Any other name answers 404 library.refNotFound {ref}, the file stays on disk, and no WIP snapshot is taken. Library references are unaffected.

How

  • One owner for the rule: media.BundleResourceType(dir, name) Type in internal/media/library.go. It returns TypeUnknown for a missing entry, a directory, any .md (case-insensitive), or a file that does not sniff as an allowed type. handleMediaBundle and handleMediaDelete both call it. The server's duplicate sniffer sniffFileType is removed.
  • Docs: docs/api.md, section POST /api/media/delete, now says a bundle reference must name a file the listing shows, and its 404 line covers that case. GET /api/media/bundle is unchanged. No skill or AGENTS.md describes this route.

Tests

  • TestBundleResourceType (internal/media): jpeg, png and pdf return their types. .md names (including a jpeg named _index.md), notes.txt, .gitkeep, subdirectories (including a directory named photos.jpg) and missing names are refused.
  • TestMediaDeleteBundleRefusesNonResources: deletes of _index.md, _index.de.md, notes.txt, .gitkeep, sub and .. each answer 404 library.refNotFound with args.ref, and the files remain.
  • TestMediaDeleteBundleRefusalTakesNoWipSnapshot: after a refusal, the WIP tip is still the baseline commit.
  • TestMediaDeleteBundleAgreesWithListing: every listed ref deletes with 200, and every name on disk the listing omits gets 404 and stays.
  • The existing bundle delete and traversal tests pass unchanged.

go vet ./... and go test ./... pass (after npm run build in web/).

🤖 Generated with Claude Code

Closes #269 ## What `POST /api/media/delete` with a bundle reference used to delete any file in the page's bundle directory, including the page's own `_index.md`, `_index.<lang>.md`, `notes.txt` and dotfiles. Now it removes a file only when `GET /api/media/bundle` would list it. Any other name answers `404 library.refNotFound {ref}`, the file stays on disk, and no WIP snapshot is taken. Library references are unaffected. ## How - **One owner for the rule:** `media.BundleResourceType(dir, name) Type` in `internal/media/library.go`. It returns `TypeUnknown` for a missing entry, a directory, any `.md` (case-insensitive), or a file that does not sniff as an allowed type. `handleMediaBundle` and `handleMediaDelete` both call it. The server's duplicate sniffer `sniffFileType` is removed. - **Docs:** `docs/api.md`, section `POST /api/media/delete`, now says a bundle reference must name a file the listing shows, and its 404 line covers that case. `GET /api/media/bundle` is unchanged. No skill or `AGENTS.md` describes this route. ## Tests - `TestBundleResourceType` (internal/media): jpeg, png and pdf return their types. `.md` names (including a jpeg named `_index.md`), `notes.txt`, `.gitkeep`, subdirectories (including a directory named `photos.jpg`) and missing names are refused. - `TestMediaDeleteBundleRefusesNonResources`: deletes of `_index.md`, `_index.de.md`, `notes.txt`, `.gitkeep`, `sub` and `..` each answer 404 `library.refNotFound` with `args.ref`, and the files remain. - `TestMediaDeleteBundleRefusalTakesNoWipSnapshot`: after a refusal, the WIP tip is still the baseline commit. - `TestMediaDeleteBundleAgreesWithListing`: every listed ref deletes with 200, and every name on disk the listing omits gets 404 and stays. - The existing bundle delete and traversal tests pass unchanged. `go vet ./...` and `go test ./...` pass (after `npm run build` in `web/`). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(media): a bundle delete removes only what the bundle listing shows
All checks were successful
ci / build-and-test (pull_request) Successful in 3m8s
ci / release (pull_request) Has been skipped
692d267e34
POST /api/media/delete with {ref, page} deleted any file in the page's
bundle directory that passed the traversal guard — the page's own
_index.md, a translation, notes, dotfiles. The predicate the bundle
listing applied inline (not a directory, not .md, sniffs as an allowed
type) is now media.BundleResourceType, called by both the listing and
the delete; a name it refuses answers 404 library.refNotFound {ref},
stays on disk and takes no WIP snapshot. The server's duplicate
sniffFileType is gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

PASS — ci/build-and-test green (go vet ./..., go test ./..., web build+test). Diff matches #269's acceptance criteria: one owner media.BundleResourceType (internal/media/library.go) called by both the bundle listing and the bundle delete, server duplicate sniffFileType removed, refusal is 404 library.refNotFound before any write/snapshot, tests cover refusals, no-snapshot and listing⇔delete agreement, docs/api.md updated.

[autoland] verdict: pass PASS — ci/build-and-test green (go vet ./..., go test ./..., web build+test). Diff matches #269's acceptance criteria: one owner media.BundleResourceType (internal/media/library.go) called by both the bundle listing and the bundle delete, server duplicate sniffFileType removed, refusal is 404 library.refNotFound before any write/snapshot, tests cover refusals, no-snapshot and listing⇔delete agreement, docs/api.md updated.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle!276
No description provided.