Skip to content

shopping list writes are not atomic: a failed write truncates .shopping-list #431

Description

@dubadub

ShoppingListStore writes the .shopping-list file with std::fs::write, which truncates the target before writing. A failure part-way through leaves the user with a truncated or empty shopping list.

Where

src/server/shopping_list_store.rs:

fs::write(&self.list_path, buf).context("writing .shopping-list")

That is save_list, which sits behind add, add_menu, remove, clear — and the legacy migration.

The same file already does the right thing elsewhere: compact() stages the new .shopping-checked content in a sibling temp file, fsyncs it, and renames it into place. So the atomic pattern is present and understood; it just is not used on the .shopping-list path.

Why the migration case is the worst of them

migrate_if_needed reads the legacy .shopping_list.txt, converts it, and its first write is save_list. If that write fails part-way, the new file is truncated while the legacy file has already been renamed to a backup — so both copies are in a bad state at once.

Impact

Requires a write failure to trigger — a full disk, a revoked permission, a disconnected network mount, or the process being killed at the wrong moment. Uncommon, but the failure mode is silent data loss on a file the user has curated, and the fix is one the codebase already contains.

Suggested direction

Route save_list through the same stage-fsync-rename helper compact() uses (src/server/fs_atomic.rs).

Notes

Found while lifting the store into a library crate, where the same code becomes reachable by the Cooklang editor and other consumers. Both write paths go through the atomic helper on that branch, pinned by a test that asserts the file's inode is replaced rather than rewritten in place. Filing separately because it is a live defect on main.

Related: #429 is the same class of bug in the pantry writer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions