fix(fuse): a deleted file's trash leaves a file of the user's under the name it was sent as (B193)

This commit is contained in:
nvrl committed 2026-10-05 04:50:17 +02:00
1 parent 31863a1da1
commit 812eb3d0bc
4 files changed
+87 -3

No files matched your search

+9 -1
View File
@@ -1081,12 +1081,20 @@ impl Core {
Err(e) => return Err(e.into()), Err(e) => return Err(e.into()),
}; };
let children = self.block_on_bounded(self.drive.enumerate_nodes_light(&uids))?; let children = self.block_on_bounded(self.drive.enumerate_nodes_light(&uids))?;
// As for a create's own twin (`Core::adoptable_twin`): a node this
// mount trashed is not one, and a node in the tree was answered for.
// A file of the user's renamed to the name the create was sent under
// was taken for it, and trashed (`docs/BUGS.md` B193).
let hidden = self.hidden.lock().clone();
let Some(twin) = children let Some(twin) = children
.into_iter() .into_iter()
.find(|node| node.name == name && !node.trashed) .find(|node| node.name == name && !node.trashed && !hidden.contains(&node.uid))
else { else {
return Ok(None); return Ok(None);
}; };
if self.state().by_uid.contains_key(&twin.uid) {
return Ok(None);
}
let queued = twin.uid.to_string(); let queued = twin.uid.to_string();
for kind in [OP_RENAME, OP_REVISION, OP_TRASH] { for kind in [OP_RENAME, OP_REVISION, OP_TRASH] {
if self.db.has_pending_op(&queued, kind)? { if self.db.has_pending_op(&queued, kind)? {
+52
View File
@@ -1736,6 +1736,58 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir); let _ = std::fs::remove_dir_all(&dir);
} }
#[test]
#[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"]
fn a_withdrawn_create_leaves_the_file_renamed_to_the_name_it_was_sent_under() {
// A create was sent as d and refused, the file renamed to q, and an
// empty file of the user's renamed to d. Deleting q left a trash that
// looked for what the create may have made under d, took the user's
// empty file for it and trashed it (B193).
let drive = FakeDrive::new();
let dir = scratch("withdrawn-create-renamed-over");
let daemon = Daemon::start(&dir, drive.client(1, Faults::lan())).unwrap();
let mnt = daemon.mountpoint.clone();
let empty = Some(Entry::File(Arc::new(Vec::new())));
std::fs::write(mnt.join("e.md"), b"").unwrap();
assert!(
wait_until(Duration::from_secs(30), || {
daemon.pending().is_ok_and(|items| items.is_empty())
&& drive.tree().get("e.md").cloned() == empty
}),
"{:?} {:?}",
daemon.pending(),
drive.tree()
);
drive.set_quota(0);
std::fs::write(mnt.join("d"), b"refused").unwrap();
assert!(wait_until(Duration::from_secs(30), || {
daemon
.pending()
.is_ok_and(|items| items.iter().any(|item| item.attempts > 0))
}));
std::fs::rename(mnt.join("d"), mnt.join("q")).unwrap();
std::fs::rename(mnt.join("e.md"), mnt.join("d")).unwrap();
assert!(wait_until(Duration::from_secs(30), || {
drive.tree().get("d").cloned() == empty
}));
std::fs::remove_file(mnt.join("q")).unwrap();
assert!(
wait_until(Duration::from_secs(30), || {
daemon.pending().is_ok_and(|items| items.is_empty())
}),
"{:?}",
daemon.pending()
);
let tree = drive.tree();
assert_eq!(tree.get("d").cloned(), empty, "{tree:?}");
assert_eq!(tree.len(), 1, "{tree:?}");
assert_eq!(std::fs::read(mnt.join("d")).unwrap(), b"");
assert!(matches!(daemon.stop(), Ok(MountOutcome::Shutdown)));
let _ = std::fs::remove_dir_all(&dir);
}
#[test] #[test]
#[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"] #[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"]
fn a_file_made_under_the_name_of_a_create_on_the_wire_waits_for_it() { fn a_file_made_under_the_name_of_a_create_on_the_wire_waits_for_it() {
+23
View File
@@ -12,6 +12,29 @@ Conventions:
--- ---
## B193 — Deleting a file whose create failed can trash a file of the user's on Drive
**Status:** Fixed (unverified).
**Found:** 2026-10-05, in the confirmation run on 1436c5f (`one_client_on_a_flaky_link` seed 6;
no replay reproduced it). A file renamed over `y/y/d`, then emptied, was gone from Drive and
from the local database, with nothing queued. The run kept no log. The path below explains it,
and the test reproduces that path every time.
**Where:** `Core::withdrawn_twin_in` in `crates/pdfs-fuse/src/drain.rs`.
**Cause.** A delete turns a create that was sent into a trash of what it may have made, under a
name it was sent with (B151, B184). The drain looks there for a node made in the last two
minutes that holds bytes the create sent, or none. It did not leave out a node in the tree, so
an empty file of the user's renamed to that name matched, and was trashed. A create's own search
for its twin (`Core::adoptable_twin`) already left such a node out, and nodes this mount trashed
(B162).
**Fix.** The trash's search leaves out the same nodes. A file a lost answer made that a listing
has since put in the tree stays on Drive, where the user can see and delete it.
**Test:** simulation test `a_withdrawn_create_leaves_the_file_renamed_to_the_name_it_was_sent_under`
(failed before the fix: Drive was empty).
## B192 — A new file closed empty reaches Drive only half a minute later ## B192 — A new file closed empty reaches Drive only half a minute later
**Status:** Fixed. **Status:** Fixed.
+3 -2
View File
@@ -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 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 - **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). locally and sent from the queue; `"local_first": false` brings back the old path (5e0f30c).
- Fixes from the simulation and account runs since then: B125, B127, B130 and B141 to B192. - Fixes from the simulation and account runs since then: B125, B127, B130 and B141 to B193.
- **Sync issues, first cut** (§6.2): refusals sorted into issues, shown at once in the app, - **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`; Discard button and tray and CLI; Export button and `pdfs sync export`; `pdfs sync issues`; Discard button and
`pdfs sync discard`; a warning badge on the node in the file browser. `pdfs sync discard`; a warning badge on the node in the file browser.
@@ -119,7 +119,8 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase
saved just before a stop whose release the stop dropped (B190, fixed; older too). The run saved just before a stop whose release the stop dropped (B190, fixed; older too). The run
on 1436c5f found a file deleted after its failed create was written again left on Drive on 1436c5f found a file deleted after its failed create was written again left on Drive
(B191, fixed). Its test for B193 found a new empty file waiting out the idle poll (B192, (B191, fixed). Its test for B193 found a new empty file waiting out the idle poll (B192,
fixed). fixed), and seed 6 a deleted file's trash taking a file of the user's renamed to the name
its create was sent under (B193, fixed).
- [ ] A clean account run. The one on a6f36d5 (2026-10-04, LAN: 427 passed, 12 failed) - [ ] A clean account run. The one on a6f36d5 (2026-10-04, LAN: 427 passed, 12 failed)
failed B113 and B121 on every on-demand mount, two mirror-to-mirror move cases, and failed B113 and B121 on every on-demand mount, two mirror-to-mirror move cases, and
the journal check. B121 was a bug (B149, fixed); B113's case still expected creates the journal check. B121 was a bug (B149, fixed); B113's case still expected creates