Make workspace/project file sync propagate deletions instead of resurrecting them

The old sync compared only the two live sides (project rows vs workspace
files), so a file present in the project but missing on disk was
indistinguishable from "never materialized here yet" - it always got
re-exported, which is why deleting a file inside a container made it come
back. The mirror direction had the same bug: a file deleted from the
project's file editor was silently re-imported from the container's stale
copy on the next tick.

Fixes it with a persisted per-file sync baseline (new project_file_sync_state
table: db_epoch/fs_epoch as they stood right after the previous sync), the
same role a rsync/Unison state file plays in any real bidirectional sync.
Deleting on either side now propagates to the other, unless the deleted
side's counterpart was edited after the last sync, in which case the edit
wins and the file is restored. A read-only project always exports (never
imports, including on tie) and always removes a workspace's stale local
copy, so it stays a faithful mirror. Sync of an unchanged file is now a true
no-op (zero writes) instead of rewriting it every ~60s tick forever.

sync_dir_bidirectional's return dict gains deleted_in_project/
deleted_in_workspace alongside exported/imported; both API call sites
already pass the whole dict through untouched. The instance sync toast now
summarizes all four counts instead of just imports.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TjdKTWgWpW2SMNW8SFqxz5
This commit is contained in:
2026-09-03 08:47:57 +02:00
co-authored by Claude Sonnet 5
parent be77672c3e
commit 70ceb3cf81
6 changed files with 399 additions and 38 deletions
+16
View File
@@ -246,6 +246,22 @@ def init_db():
_index(
db, "project_files", "idx_project_files_parent", ["project_uid", "parent_path"]
)
project_file_sync_state = get_table("project_file_sync_state")
for column, example in (
("project_uid", ""),
("path", ""),
("db_epoch", 0.0),
("fs_epoch", 0.0),
):
if not project_file_sync_state.has_column(column):
project_file_sync_state.create_column_by_example(column, example)
_index(
db,
"project_file_sync_state",
"idx_project_file_sync_state_path",
["project_uid", "path"],
unique=True,
)
_index(db, "badges", "idx_badges_user", ["user_uid"])
_index(db, "badges", "idx_badges_user_name", ["user_uid", "badge_name"])
_drop_index(db, "idx_follows_follower")