Repository navigation
feat(server): rename a recipe or menu from the editor - #623
Merged
Merged
Conversation
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
marked this pull request as ready for review
October 3, 2026 20:03
This was referenced Oct 28, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.cookand.menufiles are rewritten to the new 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:
linknever replaces its target), or, where hard links are unavailable (FAT/exFAT, Android), a copy into acreate_newfile — 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.recipe_filelike 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.Stem.jpg,Stem.N.jpg,Stem.S.N.jpgin the four extensionscooklang-findreads. One another recipe or menu can claim stays (Pasta.2.jpgis alsoPasta.2.cook's title;Plan.jpgis shared byPlan.cookandPlan.menu). One that would land on a taken name, or on another recipe's picture name, stops the rename (409).Foo.cooktoWeekbesideWeek.menuis 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.PullParser, no extensions, ascookparses) and resolved withresolve_referencefrom the writer's folder, so./vs../is read the way the rest ofcookreads 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 inreferences_skipped, not guessed.build_treekeys 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).create_newtemp + replace), never through a link or outside the collection — those are reported..shopping-listis never written;shopping_list_stalesays when it still names the old path.Access: listed in the
WRITEStable atEditRecipes, like save and delete;write_guardcovers 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).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-targetsandcargo test --workspacepass. Playwright chromium: all editor specs pass; on the full run the only failures werestatic-file-search(needs./target/debug/cookin the checkout, absent in my setup) and shopping-list specs that race on the shared.shopping-listacross workers — they pass with--workers=1and are untouched here.Left out
🤖 Generated with Claude Code