mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-04 08:56:59 +01:00
fix(fuse,core): adopt a twin holding the create's bytes (B127); trash the node of a withdrawn create that lost its answer (B151)
A retried create now adopts the file its lost answer made when Drive's SHA-1 matches the staged blob, not only when the file is empty. A delete after a sent create keeps the op as a trash that finds and trashes the node that create may have made; a new create of that name waits for it. The simulation's settle now reads the log, so known damage is explained.
This commit is contained in:
1 parent
eb1c105bbf
commit
6d75a56b00
12 files changed
+425
-51
No files matched your search
@@ -271,18 +271,21 @@ impl Db {
|
||||
/// An op that found the name still held by this trash failed with a
|
||||
/// backoff, and the name is free now. It is made due at once
|
||||
/// ([`Db::wake_ops_waiting_for`]).
|
||||
pub fn complete_trash_op(&self, op_id: i64, uid: &NodeUid) -> Result<()> {
|
||||
///
|
||||
/// Returns the staged blob of the create the trash withdrew, if it kept
|
||||
/// one, for the caller to discard.
|
||||
pub fn complete_trash_op(&self, op_id: i64, uid: &NodeUid) -> Result<Option<String>> {
|
||||
let uid = uid.to_string();
|
||||
let mut conn = self.conn.lock();
|
||||
let tx = conn.transaction()?;
|
||||
let name: Option<String> = tx
|
||||
let (name, blob): (Option<String>, Option<String>) = tx
|
||||
.query_row(
|
||||
"SELECT name FROM pending_op WHERE id = ?1",
|
||||
"SELECT name, blob_path FROM pending_op WHERE id = ?1",
|
||||
params![op_id],
|
||||
|row| row.get(0),
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)
|
||||
.optional()?
|
||||
.flatten();
|
||||
.unwrap_or_default();
|
||||
let rowid: Option<i64> = tx
|
||||
.query_row(
|
||||
"SELECT rowid FROM nodes WHERE uid = ?1",
|
||||
@@ -299,7 +302,7 @@ impl Db {
|
||||
wake_ops_waiting_for_tx(&tx, &name)?;
|
||||
}
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
Ok(blob)
|
||||
}
|
||||
|
||||
/// Check if a folder node has any non-trashed children in the database.
|
||||
|
||||
@@ -101,6 +101,12 @@ pub fn op_supersedes(kind: &str) -> bool {
|
||||
/// holds the name, as any queued trash does ([`Db::has_pending_trash_named`]):
|
||||
/// a rename over the file sent first found the name taken and landed under a
|
||||
/// conflict name (`docs/BUGS.md` B129).
|
||||
///
|
||||
/// So does a create that was sent before and failed. Its answer may have been
|
||||
/// lost after Drive made the node, which would then keep the deleted file's
|
||||
/// name and bytes (`docs/BUGS.md` B151). Such a trash keeps the create's
|
||||
/// parent and blob, which the drain looks for the node by, and which it
|
||||
/// discards once the trash is done ([`Db::complete_trash_op`]).
|
||||
fn drop_doomed_ops(tx: &rusqlite::Transaction<'_>, uid: &str) -> Result<Vec<String>> {
|
||||
const DOOMED: &str = "
|
||||
WITH RECURSIVE
|
||||
@@ -123,11 +129,13 @@ fn drop_doomed_ops(tx: &rusqlite::Transaction<'_>, uid: &str) -> Result<Vec<Stri
|
||||
WHERE uid IN (SELECT uid FROM queued)
|
||||
OR (kind = 'revision' AND uid IN (SELECT uid FROM below))
|
||||
)";
|
||||
const SENT: &str = "kind IN ('create', 'mkdir') AND (claimed_at <> 0 OR attempts > 0)";
|
||||
let blobs: Vec<String> = {
|
||||
let mut stmt = tx.prepare(&format!(
|
||||
"{DOOMED}
|
||||
SELECT blob_path FROM pending_op
|
||||
WHERE id IN (SELECT id FROM doomed) AND blob_path IS NOT NULL"
|
||||
WHERE id IN (SELECT id FROM doomed) AND blob_path IS NOT NULL
|
||||
AND NOT ({SENT})"
|
||||
))?;
|
||||
let rows = stmt.query_map(params![uid], |r| r.get::<_, String>(0))?;
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()?
|
||||
@@ -136,9 +144,10 @@ fn drop_doomed_ops(tx: &rusqlite::Transaction<'_>, uid: &str) -> Result<Vec<Stri
|
||||
&format!(
|
||||
"{DOOMED}
|
||||
UPDATE pending_op
|
||||
SET kind = 'trash', parent_uid = NULL, blob_path = NULL, meta_json = NULL
|
||||
WHERE id IN (SELECT id FROM doomed)
|
||||
AND kind IN ('create', 'mkdir') AND claimed_at <> 0"
|
||||
SET kind = 'trash', issue = NULL,
|
||||
parent_uid = CASE WHEN parent_uid LIKE '{LOCAL_VOLUME}~%' THEN NULL
|
||||
ELSE parent_uid END
|
||||
WHERE id IN (SELECT id FROM doomed) AND {SENT}"
|
||||
),
|
||||
params![uid],
|
||||
)?;
|
||||
@@ -147,7 +156,7 @@ fn drop_doomed_ops(tx: &rusqlite::Transaction<'_>, uid: &str) -> Result<Vec<Stri
|
||||
"{DOOMED}
|
||||
DELETE FROM pending_op
|
||||
WHERE id IN (SELECT id FROM doomed)
|
||||
AND NOT (kind = 'trash' AND claimed_at <> 0 AND uid LIKE '{LOCAL_VOLUME}~%')"
|
||||
AND NOT (kind = 'trash' AND uid LIKE '{LOCAL_VOLUME}~%')"
|
||||
),
|
||||
params![uid],
|
||||
)?;
|
||||
@@ -358,6 +367,28 @@ impl Db {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Whether the trash of a create a delete withdrew after it was sent is
|
||||
/// queued for `name` in `parent_uid` ([`drop_doomed_ops`]).
|
||||
///
|
||||
/// The drain looks for the node that create may have made by its folder
|
||||
/// and name, so a create of that name there waits for the trash: sent
|
||||
/// first, it made a node the trash took for the withdrawn one.
|
||||
pub fn has_withdrawn_create(&self, parent_uid: &str, name: &str) -> Result<bool> {
|
||||
let conn = self.read();
|
||||
conn.query_row(
|
||||
&format!(
|
||||
"SELECT EXISTS(
|
||||
SELECT 1 FROM pending_op
|
||||
WHERE kind = ?1 AND parent_uid = ?2 AND name = ?3
|
||||
AND uid LIKE '{LOCAL_VOLUME}~%'
|
||||
)"
|
||||
),
|
||||
params![OP_TRASH, parent_uid, name],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Whether a queued rename moves a node away from `name`.
|
||||
///
|
||||
/// Like a queued trash ([`Db::has_pending_trash_named`]) it frees the name
|
||||
|
||||
@@ -3581,6 +3581,59 @@ fn a_create_withdrawn_while_it_uploads_queues_a_trash_of_the_landed_node() {
|
||||
);
|
||||
}
|
||||
|
||||
/// A file deleted after its create was sent and failed may still have been
|
||||
/// made: the answer can be lost after Drive made it. Its trash keeps the
|
||||
/// create's folder and blob to find that node by, and holds the name in the
|
||||
/// folder until it is done (`docs/BUGS.md` B151). A create never sent goes,
|
||||
/// blob and all.
|
||||
#[test]
|
||||
fn a_create_withdrawn_after_it_failed_queues_a_trash_that_keeps_its_blob() {
|
||||
let db = Db::open_in_memory().unwrap();
|
||||
let parent = uid("parent").to_string();
|
||||
let create = |local: &str, blob: &str| {
|
||||
db.enqueue_op(&PendingOp {
|
||||
id: 0,
|
||||
kind: OP_CREATE.to_string(),
|
||||
uid: local.to_string(),
|
||||
parent_uid: Some(parent.clone()),
|
||||
name: Some("f.txt".to_string()),
|
||||
blob_path: Some(blob.to_string()),
|
||||
meta_json: Some("{}".to_string()),
|
||||
created_at: 1,
|
||||
attempts: 0,
|
||||
last_error: None,
|
||||
next_attempt_at: 0,
|
||||
})
|
||||
.unwrap()
|
||||
.0
|
||||
};
|
||||
let sent = create("local~sent", "/staging/sent");
|
||||
create("local~unsent", "/staging/unsent");
|
||||
db.record_op_failure(sent, "no answer", 0).unwrap();
|
||||
|
||||
assert!(db.delete_ops_for_uid("local~sent").unwrap().is_empty());
|
||||
assert_eq!(
|
||||
db.delete_ops_for_uid("local~unsent").unwrap(),
|
||||
vec!["/staging/unsent".to_string()]
|
||||
);
|
||||
|
||||
let ops = db.pending_ops().unwrap();
|
||||
assert_eq!(ops.len(), 1);
|
||||
assert_eq!(ops[0].kind, OP_TRASH);
|
||||
assert_eq!(ops[0].uid, "local~sent");
|
||||
assert_eq!(ops[0].parent_uid.as_deref(), Some(parent.as_str()));
|
||||
assert_eq!(ops[0].blob_path.as_deref(), Some("/staging/sent"));
|
||||
assert!(db.has_withdrawn_create(&parent, "f.txt").unwrap());
|
||||
assert!(!db.has_withdrawn_create(&parent, "g.txt").unwrap());
|
||||
|
||||
let local = NodeUid::new(VolumeId::from("local"), LinkId::from("sent"));
|
||||
assert_eq!(
|
||||
db.complete_trash_op(sent, &local).unwrap().as_deref(),
|
||||
Some("/staging/sent")
|
||||
);
|
||||
assert!(db.pending_ops().unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_landed_placeholder_never_overwrites_the_server_copy() {
|
||||
let db = Db::open_in_memory().unwrap();
|
||||
|
||||
Reference in new issue
Block a user