fix(fuse): land a conflict copy in the root when its folder is gone (B150)

This commit is contained in:
nvrl committed 2026-10-04 04:45:48 +02:00
1 parent 50ae25bb88
commit 23dc5c3723
5 files changed
+78 -3

No files matched your search

+8 -2
View File
@@ -1585,8 +1585,14 @@ impl Core {
.map(|(_, n)| n.clone())
.unwrap_or_else(|| self.node_name(uid));
// A node the tree has forgotten still has bytes worth keeping, so the
// copy falls back to the root rather than being abandoned.
let Some(parent) = place.map(|(p, _)| p).or_else(|| self.root_uid()) else {
// copy falls back to the root rather than being abandoned. So does one
// whose folder went with it: an upload into a folder Drive no longer
// has fails on every retry (B150).
let folder = match place.map(|(p, _)| p) {
Some(p) if self.fetch_node_remote(&p)?.is_some_and(|n| !n.trashed) => Some(p),
_ => None,
};
let Some(parent) = folder.or_else(|| self.root_uid()) else {
return self.abandon_to_staging(op, blob, uid, &name, reason);
};
warn!(%uid, name, reason, "queued write conflicts; keeping a conflict copy");
+47
View File
@@ -278,6 +278,7 @@ pub(crate) fn wait_until(limit: Duration, mut done: impl FnMut() -> bool) -> boo
mod tests {
use super::*;
use crate::sim::fake_drive::{Entry, FakeDrive, Faults};
use std::collections::BTreeMap;
#[test]
#[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"]
@@ -534,6 +535,52 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
#[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"]
fn a_queued_write_whose_folder_was_trashed_lands_in_the_root() {
// The conflict copy of a write to a file trashed elsewhere went into
// the file's folder. With that folder gone too, the upload failed on
// every retry, every two minutes (B150).
let drive = FakeDrive::new();
let dir = scratch("conflict-copy-folder-gone");
let daemon = Daemon::start(&dir, drive.client(1, Faults::lan())).unwrap();
let mount = &daemon.mountpoint;
std::fs::create_dir(mount.join("d")).unwrap();
std::fs::write(mount.join("d/f.txt"), b"first").unwrap();
assert!(wait_until(Duration::from_secs(30), || {
drive.tree().contains_key("d/f.txt") && daemon.pending().is_ok_and(|ops| ops.is_empty())
}));
let paused = |paused| Request::SetSyncPaused {
paused,
until: None,
};
daemon.request(&paused(true)).unwrap();
std::fs::write(mount.join("d/f.txt"), b"second").unwrap();
drive.device().trash("d/f.txt");
drive.device().trash("d");
daemon.request(&paused(false)).unwrap();
let copy = |tree: &BTreeMap<String, Entry>| {
tree.iter()
.find(|(path, _)| path.starts_with("f (sync-conflict "))
.map(|(_, entry)| entry.clone())
};
assert!(
wait_until(Duration::from_secs(30), || {
copy(&drive.tree()) == Some(Entry::File(Arc::new(b"second".to_vec())))
&& daemon.pending().is_ok_and(|ops| ops.is_empty())
}),
"{:?} {:?}",
drive.tree().keys().collect::<Vec<_>>(),
daemon.pending()
);
assert!(matches!(daemon.stop(), Ok(MountOutcome::Shutdown)));
let _ = std::fs::remove_dir_all(&dir);
}
/// Make 100 folders of 10 empty files each through a mount on `faults`, and
/// time the syscalls and then the drain until Drive holds all of it.
fn time_a_thousand_files(faults: Faults) -> (Duration, Duration) {