feat(core,fuse,cli,gui): discard a queued change and undo it here

A change Drive will not take could be exported or retried, never let go
of. The Sync page's Discard button (with a confirmation) and
`pdfs sync discard <id>` drop it: a node made here goes, as if deleted
before it was sent; a node Drive has goes back to what Drive has, under
the same inode, with every change queued on it, while what is queued
below it stays. A change on the wire or an open file is refused.
This commit is contained in:
nvrl committed 2026-10-05 00:57:02 +02:00
1 parent da013c29d6
commit 9dfe4c5450
22 files changed
+1530 -710

No files matched your search

+52
View File
@@ -2633,6 +2633,58 @@ fn failed_atomic_trash_insert_keeps_prior_revision_and_blob_ownership() {
);
}
/// Discarding a change to a node Drive has drops that node's ops and nothing
/// queued below it, and nothing at all while one of them is on the wire.
#[test]
fn discarding_a_nodes_change_keeps_what_is_queued_inside_it() {
let db = Db::open_in_memory().unwrap();
db.upsert_nodes(&[
folder("root", None, "My Files"),
folder("dir", Some("root"), "dir"),
file("inner", "dir", "inner.bin", 1),
])
.unwrap();
let op = |kind: &str, link: &str, blob: Option<&str>| PendingOp {
id: 0,
kind: kind.to_string(),
uid: uid(link).to_string(),
parent_uid: None,
name: Some("renamed".to_string()),
blob_path: blob.map(str::to_string),
meta_json: Some("{}".to_string()),
created_at: 1,
attempts: 0,
last_error: None,
next_attempt_at: 0,
};
db.enqueue_op(&op(OP_RENAME, "dir", None)).unwrap();
db.enqueue_op(&op(OP_REVISION, "inner", Some("/staging/inner")))
.unwrap();
let dir = uid("dir").to_string();
assert_eq!(db.discard_ops_on_node(&dir).unwrap(), Some(vec![]));
let left: Vec<String> = db
.pending_ops()
.unwrap()
.into_iter()
.map(|op| op.uid)
.collect();
assert_eq!(left, vec![uid("inner").to_string()]);
let inner = uid("inner").to_string();
let claimed = db.claim_next_due_op(2).unwrap().unwrap();
assert_eq!(claimed.uid, inner);
assert_eq!(db.discard_ops_on_node(&inner).unwrap(), None);
assert_eq!(db.pending_ops().unwrap().len(), 1);
db.record_op_failure(claimed.id, "no answer", 0).unwrap();
db.release_op_claim(claimed.id).unwrap();
assert_eq!(
db.discard_ops_on_node(&inner).unwrap(),
Some(vec!["/staging/inner".to_string()])
);
assert!(db.pending_ops().unwrap().is_empty());
}
/// A revision op names no parent, so the queue alone cannot see that its file
/// sits inside a folder being removed. The node tree can: trashing the folder
/// has to take the uploads of the files below it along, or they fail forever