Skip to content

feat(server): rename a recipe or menu from the editor - #623

Merged
LeMyst merged 1 commit into
cooklang:mainfrom
LeMyst:feat/rename-recipe
Oct 4, 2026
Merged

LeMyst merged 1 commit into
cooklang:mainfrom
LeMyst:feat/rename-recipe

Conversation

@LeMyst

@LeMyst LeMyst commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #622.

A Rename button in the editor, next to Picture and Delete, and POST /api/recipe_rename/{*path} ({ "name": "New name" }) behind it. The file stays in its folder; its title and step pictures are renamed with it, and the references to it in the collection's .cook and .menu files are rewritten to the new name.

Rename dialog A refused name
Rename dialog A refused name

What it touches, and what it never does

A rename changes several files at once, so it is held to more than the other write endpoints:

Risk How it is ruled out
Overwriting a file Every move is hard link + unlink (link never replaces its target), or, where hard links are unavailable (FAT/exFAT, Android), a copy into a create_new file — the refusal is the file system's own, so there is no race between the check and the move. rename() is never called (#349). A failed move rolls back the ones before it. A case-only rename on a case-insensitive FS goes through a hidden temporary name.
Reaching another file The source goes through recipe_file like save/delete (#545): an existing .cook/.menu, nothing hidden, nothing outside. A source that is a link, or whose folder leads outside the collection, is refused. The new name is one plain file name: no / \ : * ? " < > |, no control or invisible characters, no leading dot, no trailing dot, no Windows device name (CON, COM1, COM¹…), and none of @ # ~ { } [ ], -- or double spaces, which a reference could not hold.
Pictures Stem.jpg, Stem.N.jpg, Stem.S.N.jpg in the four extensions cooklang-find reads. One another recipe or menu can claim stays (Pasta.2.jpg is also Pasta.2.cook's title; Plan.jpg is shared by Plan.cook and Plan.menu). One that would land on a taken name, or on another recipe's picture name, stops the rename (409).
Changing what a reference means Renaming Foo.cook to Week beside Week.menu is refused: @./Week{} would reach the recipe before the menu. When the new name needs it (a . in it, or a menu beside a recipe of its new name), rewritten references spell the extension out.
Rewriting the wrong text References are found with the parser's own spans (PullParser, no extensions, as cook parses) and resolved with resolve_reference from the writer's folder, so ./ vs ../ is read the way the rest of cook reads it. Only the last segment is replaced, and only when it is written exactly as the old name; every other byte stays. A reference with an escape, comment or double space in its name, another letter case, or a ./Pasta/. spelling is reported in references_skipped, not guessed.
Missing a reference The collection is walked directly (build_tree keys a folder's entries by title, so two recipes with one title, or a recipe beside a folder of its name, hide each other). A collection that cannot be read in full is not renamed (500, nothing moved).
Writing where it must not A referencing file is rewritten only if it still holds what was read, atomically (hidden create_new temp + replace), never through a link or outside the collection — those are reported. .shopping-list is never written; shopping_list_stale says when it still names the old path.
Half-done renames, lost saves The work runs in a spawned task, so a client that goes away cannot stop it between two moves. Save, delete and picture writes share its lock. The editor saves pending text first and holds autosave back — otherwise a late autosave PUTs the old path and recreates the old file.

Access: listed in the WRITES table at EditRecipes, like save and delete; write_guard covers it (POST, JSON body).

Tests

  • tests/recipe_rename_api_test.rs (9): every refusal compares the whole collection byte for byte before and after; pictures, menus, sub-folder ../ references, title clashes, an unreadable folder, symlinks (unix).
  • Unit tests for the name validator, the reference rewriter (byte-exact output, CRLF, front matter, comments, escapes), picture ownership, move_no_replace / move_file / write_replace, the capability table.
  • server_auth_test.rs: guest 401, reader/shopper 403, editor/admin allowed.
  • tests/e2e/editor-rename.spec.ts: the dialog, the autosave race, refused names.

cargo fmt, cargo clippy --all-targets and cargo test --workspace pass. Playwright chromium: all editor specs pass; on the full run the only failures were static-file-search (needs ./target/debug/cook in the checkout, absent in my setup) and shopping-list specs that race on the shared .shopping-list across workers — they pass with --workers=1 and are untouched here.

Left out

  • Moving a file to another folder (same-folder only, on purpose).
  • Other open editor tabs still hold the old path; their next autosave would recreate it, since PUT creates a missing file. Same as with delete today.
  • References in the saved shopping list are reported, not rewritten.

🤖 Generated with Claude Code

A Rename button in the editor, next to Picture and Delete, and
POST /api/recipe_rename/{*path} behind it. The file stays in its folder;
its title and step pictures are renamed with it, and the references to it
in the collection's .cook and .menu files are rewritten to the new name.

Nothing is ever overwritten or touched beyond that:

- moves link-then-unlink (or copy into a create_new file where hard links
  are unavailable), so a taken name is refused by the file system itself,
  with no race between check and move; a failed move rolls back the rest;
- the source goes through recipe_file like every recipe endpoint, links
  and folders leading outside the collection are refused, and the new
  name is one plain file name (no separators, hidden, Windows-reserved or
  reference-breaking characters);
- a picture another recipe or menu can claim stays; one that would land
  on a taken name or on another recipe's picture name stops the rename;
- renaming a recipe to the name of a menu beside it is refused, since
  @./Name{} would then reach the recipe instead of the menu;
- references are found with the parser's own spans and resolve_reference,
  and only the name's last segment is replaced; anything not exact is
  reported, not guessed. The walk is our own (build_tree keys entries by
  title and drops duplicates), and a collection that cannot be read in
  full is not renamed. Files are rewritten only if unchanged since read,
  never through a link or outside the collection;
- .shopping-list is left alone; the response says when it still names
  the old path;
- the work runs in a spawned task, so a dropped request cannot stop it
  half way, and save, delete and picture writes share its lock.

The editor saves pending text first and holds autosave back, so a late
autosave cannot recreate the old file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LeMyst
LeMyst marked this pull request as ready for review October 3, 2026 20:03
@LeMyst
LeMyst merged commit 2c6e839 into cooklang:main Oct 4, 2026
7 checks passed
LeMyst added a commit that referenced this pull request Oct 28, 2026
…tab (#809)

An editor tab still open on a recipe that was renamed or deleted
elsewhere (another tab, another device) autosaved to its old path, and
`PUT /api/recipes` creates a missing file: the old name came back beside
the new one, or the deleted recipe reappeared. The editor's own rename
was already guarded; other tabs were not.

The editor now saves with `If-Match: *`, and the save endpoint refuses
it with `412` when the file is missing, checked under the lock rename
and delete take. The editor then stops saving, says the file was
renamed or deleted elsewhere, and keeps the text as unsaved, so leaving
asks first. `PUT` without the header still creates a file.

Since in-editor rename came in with 2c6e839 (#623).


Claude-Session: https://claude.ai/code/session_01MmrM91dHzvQoaerCcR9wra

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rename a recipe or menu from the web editor

1 participant