diff --git a/crates/pdfs-fuse/src/sync.rs b/crates/pdfs-fuse/src/sync.rs index 9c9ff3d..12cf67d 100644 --- a/crates/pdfs-fuse/src/sync.rs +++ b/crates/pdfs-fuse/src/sync.rs @@ -776,7 +776,23 @@ impl Core { } match plan_file(l, r, baseline.get(rel)) { - FilePlan::Unchanged => {} + // The size was read for real for a row without a revision id + // (B254), and still matches: give the row the id, so later + // walks trust the recorded size again. + FilePlan::Unchanged => { + if let (Some(base), Some(revision)) = + (baseline.get(rel), r.and_then(|r| r.revision.as_ref())) + && base.remote_revision_id.is_none() + { + let row = StoredSyncEntry { + remote_revision_id: Some(revision.clone()), + ..base.clone() + }; + if let Err(e) = self.db.sync_entry_upsert(folder_id, &row) { + debug!(rel, error = ?e, "sync: cannot record the revision id"); + } + } + } FilePlan::AdoptBaseline => { // First sync of a file the same size on both sides: the apply // step hashes the local copy and records the baseline only if diff --git a/crates/pdfs-fuse/src/sync/planner.rs b/crates/pdfs-fuse/src/sync/planner.rs index 8fe5fa6..95b6f04 100644 --- a/crates/pdfs-fuse/src/sync/planner.rs +++ b/crates/pdfs-fuse/src/sync/planner.rs @@ -333,6 +333,13 @@ pub(super) fn remote_subtree_changed( /// The size the baseline recorded for `rel`, if its remote signature's mtime /// still matches `mtime` and its revision is still `revision`. +/// +/// A row without a revision id, while Drive reports one, cannot vouch for the +/// revision, so the size is read for real. Such a row comes from before +/// schema 44, or from an upload Drive did not list yet when it was read back. +/// Another computer's revision in the same second as that upload kept the +/// modification time, and only the size could show it (B254). The pass then +/// records the id on the row, so this costs one read per row. pub(super) fn unchanged_remote_size( baseline: &HashMap, rel: &str, @@ -340,7 +347,9 @@ pub(super) fn unchanged_remote_size( revision: Option<&str>, ) -> Option { let base = baseline.get(rel)?; - if revision_moved(Some(base), revision) { + if revision_moved(Some(base), revision) + || (base.remote_revision_id.is_none() && revision.is_some()) + { return None; } match remote_sig(base) { @@ -756,6 +765,15 @@ mod file_plan_tests { assert_eq!(unchanged_remote_size(&rows, "file", 31, Some("r1")), None); } + /// A row without a revision id reuses its size only while Drive gives no + /// id either (B254). + #[test] + fn a_row_without_a_revision_makes_the_walk_read_the_size_again() { + let rows = HashMap::from([("file".to_string(), baseline(10, 20, 30, 40))]); + assert_eq!(unchanged_remote_size(&rows, "file", 30, Some("r1")), None); + assert_eq!(unchanged_remote_size(&rows, "file", 30, None), Some(40)); + } + #[test] fn a_missing_remote_signature_is_treated_as_changed() { let local = local(10, 20); diff --git a/docs/BUGS.md b/docs/BUGS.md index 92b99e3..aad9d31 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -100,6 +100,7 @@ a live account since. | Entry | Title | |---|---| +| [B254](#b254--an-edit-in-the-same-second-as-a-new-upload-never-arrives) | An edit in the same second as a new upload never arrives | | [B253](#b253--the-app-shows-the-sign-in-page-when-the-keyring-is-locked) | The app shows the sign-in page when the keyring is locked | | [B252](#b252--computers-sharing-a-device-overwrite-each-others-profile) | Computers sharing a device overwrite each other's profile | | [B251](#b251--two-computers-can-make-the-same-conflict-copy-name) | Two computers can make the same conflict copy name | @@ -356,6 +357,33 @@ a live account since. Newest first. +### B254 — An edit in the same second as a new upload never arrives + +**Status:** Fixed, not verified live +**Found:** 2026-10-16, the B250 simulation failing about one run in five on CI. +**Where:** `unchanged_remote_size` in `crates/pdfs-fuse/src/sync/planner.rs`; the +`FilePlan::Unchanged` arm of `do_reconcile` in `crates/pdfs-fuse/src/sync.rs` + +**Repro:** A mirror pass uploads a new file, and Drive does not list it yet when the pass +reads it back. Another computer saves a new revision with the same modification time, to the +second. The mirror keeps the old content, and no pass ever downloads the new one. + +**Cause.** The read-back found no node, so the baseline row got the local modification time +and size but no revision id. The next walk reused the recorded size whenever the modification +time matched and the revision id had not moved. A row without an id never counts as moved, so +the walk never read the new size, and the file looked unchanged. + +**Fix.** A row without a revision id reuses its size only while Drive gives no id either. +Otherwise the walk reads the size for real. If the file is still unchanged, the pass writes +Drive's revision id into the row, so the cost is one extra read per row. Rows from before +schema 44 have no id either, so the first pass after upgrading reads every file's size once. + +**Test:** `a_row_without_a_revision_makes_the_walk_read_the_size_again` in `pdfs-fuse`. The +simulation `a_revision_made_during_a_mirror_download_still_arrives` failed 3 of about 15 runs +before the fix and passed 25 of 25 after it. + +--- + ### B253 — The app shows the sign-in page when the keyring is locked **Status:** Fixed, not verified live diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6fb963c..5ddcb5c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -31,6 +31,9 @@ version on Proton Drive it was synced at. Nothing is rewritten, and the upgrade - **The app showed the sign-in page when the system keyring was locked or not running yet**, as if the sign-in had been lost. It now says that the keyring could not be read, and opens on its own once the keyring answers (B253). +- **A new version saved on another computer in the same second as an upload from here could + be missed for good** when Drive had not listed the uploaded file yet. The first sync pass + after upgrading reads each file's size from Drive once more to close this (B254). - **An edit in a synced folder could replace one another computer had just saved.** The newer version from the other computer was left only in the version history. The edit is now kept as a conflict copy (B249).