From 0f3c06f982a86ef1d554ef7f3f90e969a556c090 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sun, 4 Oct 2026 22:41:57 +0200 Subject: [PATCH] fix(core): a folder listed just as its create lands is listed from the database (B177) --- crates/pdfs-core/src/db/nodes.rs | 21 +++++++++++++++++---- crates/pdfs-core/src/db/tests.rs | 31 +++++++++++++++++++++++++++++++ docs/BUGS.md | 22 ++++++++++++++++++++++ docs/CHANGELOG.md | 2 ++ docs/MILESTONE-3.0.0-STATUS.md | 4 +++- 5 files changed, 75 insertions(+), 5 deletions(-) diff --git a/crates/pdfs-core/src/db/nodes.rs b/crates/pdfs-core/src/db/nodes.rs index 21a2dda..ae8b26e 100644 --- a/crates/pdfs-core/src/db/nodes.rs +++ b/crates/pdfs-core/src/db/nodes.rs @@ -533,9 +533,10 @@ impl Db { /// re-enumerates from the remote on next access. pub fn set_listed(&self, uid: &NodeUid, listed: bool) -> Result<()> { let conn = self.conn.lock(); + let (key, uid) = row_key(&uid.to_string()); conn.execute( - "UPDATE nodes SET listed = ?2 WHERE uid = ?1", - params![uid.to_string(), listed as i64], + &format!("UPDATE nodes SET listed = ?2 WHERE {key} = ?1"), + params![uid, listed as i64], )?; Ok(()) } @@ -694,10 +695,11 @@ impl Db { /// server head, so changes made while unmounted are still applied (P2). pub fn children_if_listed(&self, parent: &NodeUid) -> Result>> { let conn = self.read(); + let (key, folder) = row_key(&parent.to_string()); let listed: Option = conn .query_row( - "SELECT listed FROM nodes WHERE uid = ?1", - params![parent.to_string()], + &format!("SELECT listed FROM nodes WHERE {key} = ?1"), + params![folder], |r| r.get(0), ) .optional()?; @@ -792,6 +794,17 @@ pub(super) fn node_lid(conn: &rusqlite::Connection, uid: &str) -> Result (&'static str, Value) { + match local_lid(uid) { + Some(lid) => ("lid", Value::Integer(lid)), + None => ("uid", Value::Text(uid.to_owned())), + } +} + /// The column and value a row names `parent` by: its local id when it has a /// row, which the children keep when it lands, or its uid when it has none, /// as a device folder's root does. diff --git a/crates/pdfs-core/src/db/tests.rs b/crates/pdfs-core/src/db/tests.rs index fd47dbc..484da40 100644 --- a/crates/pdfs-core/src/db/tests.rs +++ b/crates/pdfs-core/src/db/tests.rs @@ -3282,6 +3282,37 @@ fn a_stand_in_keeps_its_access_once_its_create_has_landed() { ); } +#[test] +fn a_stand_in_folder_stays_listed_once_its_create_has_landed() { + let db = Db::open_in_memory().unwrap(); + db.upsert_node(&folder("parent", None, "My Files")).unwrap(); + let parent = uid("parent").to_string(); + let lid = db.add_local_row(&parent, "dir", true, 1).unwrap(); + let local = local_uid(lid); + let stand_in = NodeUid::new(VolumeId::from("local"), LinkId::from(lid.to_string())); + let mut node = folder("new", Some("parent"), "dir"); + node.uid = stand_in.clone(); + db.upsert_node(&node).unwrap(); + db.set_listed(&stand_in, true).unwrap(); + let real = uid("real").to_string(); + let (id, _) = db + .enqueue_op(&op_on(OP_MKDIR, &local, Some(&parent))) + .unwrap(); + db.claim_next_due_op(1).unwrap().unwrap(); + db.finish_create(id, None, &landing(&local, &real, &parent, "dir"), |_| None) + .unwrap(); + + // The tree still names the folder by its stand-in for a moment; a listing + // asked by it must not go to Drive, which has never heard of it. + assert!( + db.children_if_listed(&stand_in) + .unwrap() + .is_some_and(|kids| kids.is_empty()) + ); + db.set_listed(&stand_in, false).unwrap(); + assert!(db.children_if_listed(&uid("real")).unwrap().is_none()); +} + #[test] fn a_node_queued_under_both_its_uids_is_sent_one_op_at_a_time() { let db = Db::open_in_memory().unwrap(); diff --git a/docs/BUGS.md b/docs/BUGS.md index a2aa149..aa5ef24 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -12,6 +12,28 @@ Conventions: --- +## B177 — A folder listed just as its create lands answers "Input/output error" + +**Status:** Fixed (unverified). +**Found:** 2026-10-04, by the account run on 378667f, on an on-demand sync folder: B100's +`lstat` of `b100-copy/dir-0/sub-0/file-00.bin` failed with `EIO`. The daemon logged +`enumerate folder children failed folder_uid=local~163903 ... InvalidEncryptedIdFormat` 85 ms +before `dir-0`'s create landed. + +**Where:** `Db::children_if_listed` and `Db::set_listed` in `crates/pdfs-core/src/db/nodes.rs`. + +**Cause.** A folder made on the mount is marked as listed, so its listing comes from the +database and never from Drive, which has not heard of it. The flag was looked up by uid. In +B173's gap the row already has the uid Drive gave the folder and the tree still asks by the +stand-in, so the lookup found no row, and the listing went to Drive with the stand-in. + +**Fix.** A stand-in finds its row by its local id, before and after its create lands. + +**Test:** `a_stand_in_folder_stays_listed_once_its_create_has_landed` in +`crates/pdfs-core/src/db/tests.rs`. + +--- + ## B176 — A write closed just as its file's create lands is dropped **Status:** Fixed (unverified). diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 914ccb1..2ca1ad2 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -79,6 +79,8 @@ by its local id, in the queue, the stored tree and pins alike. - **A file replaced by a rename just as it reached Drive is removed from Drive too.** It no longer stayed there, and the next rename of its name no longer moved it instead (B175). - **A write closed just as its file reached Drive is no longer lost** (B176). +- **Opening a folder just as it reaches Drive no longer fails with "Input/output error"** + (B177). - **A file renamed just as its upload finished keeps its new name.** The mount no longer showed the old name, and a second rename no longer failed with "No such file or directory" (B152, B172). diff --git a/docs/MILESTONE-3.0.0-STATUS.md b/docs/MILESTONE-3.0.0-STATUS.md index 4adb644..e679ced 100644 --- a/docs/MILESTONE-3.0.0-STATUS.md +++ b/docs/MILESTONE-3.0.0-STATUS.md @@ -9,7 +9,7 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase - **Phase 2**: local ids, inode = local id, one tree for every mount (2.10.0). - **Phase 3, first part**: mkdir, create, rename, unlink and rmdir on the mount are recorded locally and sent from the queue; `"local_first": false` brings back the old path (5e0f30c). -- Fixes from the simulation and account runs since then: B127 and B141 to B176. +- Fixes from the simulation and account runs since then: B127 and B141 to B177. - **Sync issues, first cut** (§6.2): refusals sorted into issues, shown at once in the app, tray and CLI; Export button and `pdfs sync export`; `pdfs sync issues`. @@ -121,6 +121,8 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase in the same gap was refused with `EACCES` (B174, fixed). The loop on 378667f failed seed 8: a file replaced by a rename in that gap stayed on Drive (B175, fixed), and seed 12: a write closed as its file's create landed was dropped (B176, fixed). + The account run on 378667f: a folder listed as its create landed asked Drive by + its stand-in and answered `EIO` (B177, fixed). 5. **Release**: 2.10.0 is tagged; the 2.9.0 tag still waits for the user's go-ahead. ## After 3.0.0