mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
refactor(core,fuse): a node made here goes by its local id until it lands (schema 41)
A file or folder made on the mount used to get a minted `local~<ms>-<n>` placeholder. It now gets `local~<lid>`, a function of the row it is given first (`Db::add_local_row`), so the stand-in never changes and always leads back to the row, and through it to the real uid once the node lands. `Db::lid_of` reads it without a lookup. Migration V41 rewrites the placeholders still stored to their row's stand-in: the row and its node_json, its children, the ops on it or into it (uid, parent, sidecar uid, a rename's original parent), and its pins. Step 6a of the plan for 3.0.0 phase 3.
This commit is contained in:
1 parent
3f74b95b34
commit
e5f3f612f8
8 files changed
+312
-58
No files matched your search
@@ -11,7 +11,7 @@ use super::Db;
|
||||
use crate::Result;
|
||||
|
||||
/// Current schema version. Bump on every forward migration added below.
|
||||
pub(super) const SCHEMA_VERSION: i64 = 40;
|
||||
pub(super) const SCHEMA_VERSION: i64 = 41;
|
||||
|
||||
impl Db {
|
||||
pub(super) fn migrate(&self) -> Result<()> {
|
||||
@@ -411,6 +411,19 @@ impl Db {
|
||||
tx.execute_batch(MIGRATION_V40)?;
|
||||
}
|
||||
}
|
||||
if current < 41 {
|
||||
// Needs both tables; on a database that never had them there is
|
||||
// nothing to rewrite.
|
||||
let tables: i64 = tx.query_row(
|
||||
"SELECT COUNT(*) FROM sqlite_master
|
||||
WHERE type = 'table' AND name IN ('nodes', 'pending_op', 'pins')",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
if tables == 3 {
|
||||
tx.execute_batch(MIGRATION_V41)?;
|
||||
}
|
||||
}
|
||||
tx.execute(
|
||||
"INSERT INTO sync_state (key, value) VALUES ('schema_version', ?1)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
||||
@@ -1245,3 +1258,47 @@ ALTER TABLE nodes ADD COLUMN parent_lid INTEGER;
|
||||
UPDATE nodes SET parent_lid = (SELECT p.lid FROM nodes p WHERE p.uid = nodes.parent_uid);
|
||||
CREATE INDEX idx_nodes_parent_lid ON nodes(parent_lid);
|
||||
";
|
||||
|
||||
/// Schema v41: a node made on this machine goes by `local~<lid>` until it
|
||||
/// lands, a function of its row, where it used to get a `local~<ms>-<n>`
|
||||
/// placeholder. Every placeholder still stored is rewritten to its row's
|
||||
/// stand-in: the row itself and its `node_json`, its children, the ops on it
|
||||
/// or into it, and its pins. A placeholder op with no row keeps its uid and
|
||||
/// drains as before.
|
||||
const MIGRATION_V41: &str = "
|
||||
CREATE TEMP TABLE local_v41 AS
|
||||
SELECT uid AS old, 'local~' || lid AS new, lid FROM nodes
|
||||
WHERE uid LIKE 'local~%' AND uid <> 'local~' || lid;
|
||||
UPDATE pending_op SET uid = (SELECT new FROM local_v41 WHERE old = pending_op.uid)
|
||||
WHERE uid IN (SELECT old FROM local_v41);
|
||||
UPDATE pending_op SET parent_uid = (SELECT new FROM local_v41 WHERE old = pending_op.parent_uid)
|
||||
WHERE parent_uid IN (SELECT old FROM local_v41);
|
||||
UPDATE pending_op
|
||||
SET meta_json = json_set(meta_json, '$.uid',
|
||||
(SELECT new FROM local_v41 WHERE old = json_extract(pending_op.meta_json, '$.uid')))
|
||||
WHERE json_valid(meta_json)
|
||||
AND json_extract(meta_json, '$.uid') IN (SELECT old FROM local_v41);
|
||||
UPDATE pending_op
|
||||
SET meta_json = json_set(meta_json, '$.original_parent_uid',
|
||||
(SELECT new FROM local_v41
|
||||
WHERE old = json_extract(pending_op.meta_json, '$.original_parent_uid')))
|
||||
WHERE json_valid(meta_json)
|
||||
AND json_extract(meta_json, '$.original_parent_uid') IN (SELECT old FROM local_v41);
|
||||
UPDATE pins SET uid = (SELECT new FROM local_v41 WHERE old = pins.uid)
|
||||
WHERE uid IN (SELECT old FROM local_v41);
|
||||
UPDATE nodes
|
||||
SET node_json = json_set(node_json, '$.parent_uid.link_id',
|
||||
(SELECT CAST(lid AS TEXT) FROM local_v41 WHERE old = nodes.parent_uid))
|
||||
WHERE parent_uid IN (SELECT old FROM local_v41)
|
||||
AND json_valid(node_json)
|
||||
AND json_extract(node_json, '$.parent_uid.volume_id') = 'local';
|
||||
UPDATE nodes SET parent_uid = (SELECT new FROM local_v41 WHERE old = nodes.parent_uid)
|
||||
WHERE parent_uid IN (SELECT old FROM local_v41);
|
||||
UPDATE nodes
|
||||
SET node_json = json_set(node_json, '$.uid.link_id', CAST(lid AS TEXT))
|
||||
WHERE lid IN (SELECT lid FROM local_v41)
|
||||
AND json_valid(node_json)
|
||||
AND json_extract(node_json, '$.uid.volume_id') = 'local';
|
||||
UPDATE nodes SET uid = 'local~' || lid WHERE lid IN (SELECT lid FROM local_v41);
|
||||
DROP TABLE local_v41;
|
||||
";
|
||||
@@ -58,7 +58,7 @@ pub use nodes::{PublishedSharedRoot, SearchHit, StoredNode};
|
||||
pub use ops::{
|
||||
AttachedBlob, CreateLanding, CreateRetired, FAILING_ATTEMPTS, LOCAL_VOLUME, OP_CREATE,
|
||||
OP_MKDIR, OP_RENAME, OP_REVISION, OP_TRASH, PARK_EXPIRY_MS, PARK_UNTIL, PendingCounts,
|
||||
PendingOp, RenameMeta, op_supersedes,
|
||||
PendingOp, RenameMeta, local_lid, local_uid, op_supersedes,
|
||||
};
|
||||
pub use photos::{StoredPhoto, StoredPlace, THUMB_HAVE, THUMB_NONE, THUMB_UNKNOWN, TimelineRow};
|
||||
pub use pins::PinRow;
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::collections::HashSet;
|
||||
use rusqlite::{OptionalExtension, Transaction, params};
|
||||
|
||||
use super::Db;
|
||||
use super::ops::{OP_CREATE, OP_MKDIR, OP_RENAME, wake_ops_waiting_for_tx};
|
||||
use super::ops::{OP_CREATE, OP_MKDIR, OP_RENAME, local_lid, local_uid, wake_ops_waiting_for_tx};
|
||||
use crate::{Access, Result};
|
||||
use proton_drive_rs::proton_sdk::ids::{LinkId, NodeUid, VolumeId};
|
||||
use proton_drive_rs::{Node, NodeKind};
|
||||
@@ -593,14 +593,21 @@ impl Db {
|
||||
|
||||
/// The local id of the node stored under `uid`, or `None` when it has no
|
||||
/// row. It stays the same for as long as the row exists, including when a
|
||||
/// create lands and the row takes the uid Drive gave it.
|
||||
/// create lands and the row takes the uid Drive gave it: a node made on
|
||||
/// this machine is found by its stand-in ([`local_uid`]) before and after.
|
||||
pub fn lid_of(&self, uid: &str) -> Result<Option<i64>> {
|
||||
let conn = self.read();
|
||||
Ok(conn
|
||||
.query_row("SELECT lid FROM nodes WHERE uid = ?1", params![uid], |r| {
|
||||
let row = match local_lid(uid) {
|
||||
Some(lid) => {
|
||||
conn.query_row("SELECT lid FROM nodes WHERE lid = ?1", params![lid], |r| {
|
||||
r.get(0)
|
||||
})
|
||||
}
|
||||
None => conn.query_row("SELECT lid FROM nodes WHERE uid = ?1", params![uid], |r| {
|
||||
r.get(0)
|
||||
})
|
||||
.optional()?)
|
||||
}),
|
||||
};
|
||||
Ok(row.optional()?)
|
||||
}
|
||||
|
||||
/// The local id of each node, in order, giving a row to each that has none.
|
||||
@@ -644,6 +651,41 @@ impl Db {
|
||||
Ok(lids)
|
||||
}
|
||||
|
||||
/// Give a node being made on this machine its row under `parent_uid`, and
|
||||
/// return its local id. The row goes by [`local_uid`] of that id until
|
||||
/// the node lands.
|
||||
///
|
||||
/// Like [`Db::lids_for`], only a stub: the caller upserts the node.
|
||||
pub fn add_local_row(
|
||||
&self,
|
||||
parent_uid: &str,
|
||||
name: &str,
|
||||
is_dir: bool,
|
||||
mtime: i64,
|
||||
) -> Result<i64> {
|
||||
let mut conn = self.conn.lock();
|
||||
let tx = conn.transaction()?;
|
||||
tx.execute(
|
||||
"INSERT INTO nodes (uid, parent_uid, name, is_dir, mtime, trashed, parent_lid)
|
||||
VALUES (NULL, ?1, ?2, ?3, ?4, 0,
|
||||
COALESCE(?5, (SELECT lid FROM nodes WHERE uid = ?1)))",
|
||||
params![
|
||||
parent_uid,
|
||||
name,
|
||||
is_dir as i64,
|
||||
mtime,
|
||||
local_lid(parent_uid)
|
||||
],
|
||||
)?;
|
||||
let lid = tx.last_insert_rowid();
|
||||
tx.execute(
|
||||
"UPDATE nodes SET uid = ?2 WHERE lid = ?1",
|
||||
params![lid, local_uid(lid)],
|
||||
)?;
|
||||
tx.commit()?;
|
||||
Ok(lid)
|
||||
}
|
||||
|
||||
/// Read the persisted incremental-sync cursor (a `DriveEventId`), if any.
|
||||
/// The daemon resumes from this on restart instead of reseeding to the
|
||||
/// server head, so changes made while unmounted are still applied (P2).
|
||||
|
||||
@@ -205,13 +205,33 @@ const PARENT_NOW: &str =
|
||||
"COALESCE((SELECT uid FROM nodes WHERE lid = pending_op.parent_lid), parent_uid)";
|
||||
|
||||
/// The volume id given to a node that exists only on this machine, so far. A
|
||||
/// real [`NodeUid`] is `{volume}~{link}`, so a placeholder is `local~<uuid>` and
|
||||
/// real [`NodeUid`] is `{volume}~{link}`, so a placeholder is `local~<lid>` and
|
||||
/// round-trips through the same `Display`/parse path as any other uid.
|
||||
///
|
||||
/// Nothing bearing this volume may be handed to the API — it would 404. The
|
||||
/// drain replaces it with the uid the server assigns.
|
||||
pub const LOCAL_VOLUME: &str = "local";
|
||||
|
||||
/// The uid a node made on this machine goes by until Drive gives it one: its
|
||||
/// row's local id under [`LOCAL_VOLUME`].
|
||||
///
|
||||
/// A function of the row, so it never changes: whatever still holds it once
|
||||
/// the node has landed finds the node, and its real uid, through
|
||||
/// [`local_lid`].
|
||||
pub fn local_uid(lid: i64) -> String {
|
||||
format!("{LOCAL_VOLUME}~{lid}")
|
||||
}
|
||||
|
||||
/// The local id a [`local_uid`] stands for, or `None` for any other uid,
|
||||
/// including a placeholder minted before schema 41.
|
||||
pub fn local_lid(uid: &str) -> Option<i64> {
|
||||
let digits = uid.strip_prefix(LOCAL_VOLUME)?.strip_prefix('~')?;
|
||||
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
digits.parse().ok()
|
||||
}
|
||||
|
||||
/// A `next_attempt_at` far enough in the future that [`Db::next_due_op`] never
|
||||
/// selects the op (ms since epoch, ≈ year 2223). Used to *park* a queued create
|
||||
/// for a transient file — a browser's in-flight `*.crdownload`/`*.part`, an
|
||||
|
||||
@@ -3897,15 +3897,25 @@ fn migration_v36_gives_every_node_its_rowid_as_lid() {
|
||||
assert_eq!(rowids.len(), 3);
|
||||
|
||||
let db = Db::open(&path).unwrap();
|
||||
// The placeholder goes by its row's stand-in from V41 on.
|
||||
let stand_in = |uid: &str, rowid: i64| match uid == local.to_string() {
|
||||
true => local_uid(rowid),
|
||||
false => uid.to_string(),
|
||||
};
|
||||
for (uid, rowid) in &rowids {
|
||||
assert_eq!(db.lid_of(uid).unwrap(), Some(*rowid), "{uid}");
|
||||
let uid = stand_in(uid, *rowid);
|
||||
assert_eq!(db.lid_of(&uid).unwrap(), Some(*rowid), "{uid}");
|
||||
}
|
||||
let found = db.search("report", 10).unwrap();
|
||||
assert_eq!(found.len(), 1, "the search index still finds the node");
|
||||
assert_eq!(found[0].node.uid, uid("kept"));
|
||||
let ops = db.pending_ops().unwrap();
|
||||
assert_eq!(ops.len(), 1);
|
||||
assert_eq!(ops[0].uid, local.to_string());
|
||||
let (_, dir) = rowids
|
||||
.iter()
|
||||
.find(|(uid, _)| *uid == local.to_string())
|
||||
.unwrap();
|
||||
assert_eq!(ops[0].uid, local_uid(*dir));
|
||||
assert_eq!(ops[0].attempts, 1);
|
||||
assert_eq!(ops[0].last_error.as_deref(), Some("network unreachable"));
|
||||
|
||||
@@ -3916,6 +3926,93 @@ fn migration_v36_gives_every_node_its_rowid_as_lid() {
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_v41_gives_every_placeholder_its_rows_stand_in() {
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"pdfs-db-v40-fixture-{}-{}.db",
|
||||
std::process::id(),
|
||||
now_test_id()
|
||||
));
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let (dir, inner) = {
|
||||
let db = Db::open(&path).unwrap();
|
||||
db.upsert_node(&folder("root", None, "My Files")).unwrap();
|
||||
let old_dir = NodeUid::new(VolumeId::from("local"), LinkId::from("1700000000000-0"));
|
||||
let old_inner = NodeUid::new(VolumeId::from("local"), LinkId::from("1700000000000-1"));
|
||||
let mut dir = folder("x", Some("root"), "Offline folder");
|
||||
dir.uid = old_dir.clone();
|
||||
db.upsert_node(&dir).unwrap();
|
||||
let mut inner = file("x", "root", "inner.txt", 0);
|
||||
inner.uid = old_inner.clone();
|
||||
inner.parent_uid = Some(old_dir.clone());
|
||||
db.upsert_node(&inner).unwrap();
|
||||
let op = |kind: &str, uid: &NodeUid, parent: &str, meta: Option<String>| PendingOp {
|
||||
id: 0,
|
||||
kind: kind.to_string(),
|
||||
uid: uid.to_string(),
|
||||
parent_uid: Some(parent.to_string()),
|
||||
name: Some("n".to_string()),
|
||||
blob_path: None,
|
||||
meta_json: meta,
|
||||
created_at: 1,
|
||||
attempts: 0,
|
||||
last_error: None,
|
||||
next_attempt_at: 0,
|
||||
};
|
||||
db.enqueue_op(&op(OP_MKDIR, &old_dir, &uid("root").to_string(), None))
|
||||
.unwrap();
|
||||
let staged = json!({"uid": old_inner.to_string(), "len": 0}).to_string();
|
||||
db.enqueue_op(&op(
|
||||
OP_CREATE,
|
||||
&old_inner,
|
||||
&old_dir.to_string(),
|
||||
Some(staged),
|
||||
))
|
||||
.unwrap();
|
||||
db.pin_add(&old_dir.to_string(), "/Offline folder", true)
|
||||
.unwrap();
|
||||
let lids = (
|
||||
db.lid_of(&old_dir.to_string()).unwrap().unwrap(),
|
||||
db.lid_of(&old_inner.to_string()).unwrap().unwrap(),
|
||||
);
|
||||
drop(db);
|
||||
rusqlite::Connection::open(&path)
|
||||
.unwrap()
|
||||
.execute(
|
||||
"UPDATE sync_state SET value = '40' WHERE key = 'schema_version'",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
lids
|
||||
};
|
||||
|
||||
let db = Db::open(&path).unwrap();
|
||||
let (dir_uid, inner_uid) = (local_uid(dir), local_uid(inner));
|
||||
assert_eq!(db.uid_of_lid(dir).unwrap(), Some(dir_uid.clone()));
|
||||
assert_eq!(db.uid_of_lid(inner).unwrap(), Some(inner_uid.clone()));
|
||||
let stored = db.node_by_uid(&inner_uid).unwrap().unwrap();
|
||||
assert_eq!(
|
||||
stored.uid.to_string(),
|
||||
inner_uid,
|
||||
"node_json follows the row"
|
||||
);
|
||||
assert_eq!(
|
||||
stored.parent_uid.map(|p| p.to_string()),
|
||||
Some(dir_uid.clone())
|
||||
);
|
||||
let ops = db.pending_ops().unwrap();
|
||||
assert_eq!(ops[0].uid, dir_uid);
|
||||
assert_eq!(ops[1].uid, inner_uid);
|
||||
assert_eq!(ops[1].parent_uid, Some(dir_uid.clone()));
|
||||
let staged: serde_json::Value =
|
||||
serde_json::from_str(ops[1].meta_json.as_deref().unwrap()).unwrap();
|
||||
assert_eq!(staged["uid"], inner_uid.as_str());
|
||||
let pins: Vec<String> = db.pin_list().unwrap().into_iter().map(|p| p.uid).collect();
|
||||
assert_eq!(pins, vec![dir_uid]);
|
||||
drop(db);
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
/// A database as 2.8.2 wrote it (`sqlite3 .dump`, schema 35), with queued ops
|
||||
/// in every state the drain leaves them in: due, parked, failing, deferred for
|
||||
/// access, and claimed by a worker when the daemon stopped. The creates below
|
||||
@@ -3935,36 +4032,45 @@ fn a_2_8_database_migrates_with_every_node_and_op_intact() {
|
||||
let (nodes, ops) = migrates_intact(&path);
|
||||
assert_eq!((nodes, ops), (8, 6));
|
||||
let db = Db::open(&path).unwrap();
|
||||
let lid_named = |name: &str| -> i64 {
|
||||
db.conn
|
||||
.lock()
|
||||
.query_row("SELECT lid FROM nodes WHERE name = ?1", [name], |r| {
|
||||
r.get(0)
|
||||
})
|
||||
.unwrap()
|
||||
};
|
||||
let (dir, inner_lid) = (lid_named("Offline folder"), lid_named("inner.txt"));
|
||||
let queued = db.pending_ops().unwrap();
|
||||
let inner = queued.iter().find(|op| op.uid == "local~inner").unwrap();
|
||||
assert_eq!(inner.parent_uid.as_deref(), Some("local~dir"));
|
||||
let inner = queued
|
||||
.iter()
|
||||
.find(|op| op.uid == local_uid(inner_lid))
|
||||
.unwrap();
|
||||
assert_eq!(inner.parent_uid, Some(local_uid(dir)));
|
||||
let lids: (Option<i64>, Option<i64>) = db
|
||||
.conn
|
||||
.lock()
|
||||
.query_row(
|
||||
"SELECT lid, parent_lid FROM pending_op WHERE uid = 'local~inner'",
|
||||
[],
|
||||
"SELECT lid, parent_lid FROM pending_op WHERE id = ?1",
|
||||
[inner.id],
|
||||
|r| Ok((r.get(0)?, r.get(1)?)),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
lids,
|
||||
(
|
||||
db.lid_of("local~inner").unwrap(),
|
||||
db.lid_of("local~dir").unwrap()
|
||||
),
|
||||
(Some(inner_lid), Some(dir)),
|
||||
"the migration names the op's node and parent by local id"
|
||||
);
|
||||
let parent_lid: Option<i64> = db
|
||||
.conn
|
||||
.lock()
|
||||
.query_row(
|
||||
"SELECT parent_lid FROM nodes WHERE uid = 'local~inner'",
|
||||
[],
|
||||
"SELECT parent_lid FROM nodes WHERE lid = ?1",
|
||||
[inner_lid],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(parent_lid, db.lid_of("local~dir").unwrap());
|
||||
assert_eq!(parent_lid, Some(dir));
|
||||
let parked = db.parked_create_ops().unwrap();
|
||||
assert_eq!(parked.len(), 1);
|
||||
assert_eq!(parked[0].name.as_deref(), Some("movie.part"));
|
||||
@@ -3997,7 +4103,7 @@ fn a_copy_of_a_real_database_migrates_with_every_node_and_op_intact() {
|
||||
/// Opens the database at `path` and checks that the migration kept every node
|
||||
/// on its row id and every queued op as it was. Returns how many of each.
|
||||
fn migrates_intact(path: &std::path::Path) -> (usize, usize) {
|
||||
let (version, nodes, ops, sealed) = {
|
||||
let (version, nodes, mut ops, sealed) = {
|
||||
let conn = rusqlite::Connection::open(path).unwrap();
|
||||
let version: String = conn
|
||||
.query_row(
|
||||
@@ -4017,6 +4123,33 @@ fn migrates_intact(path: &std::path::Path) -> (usize, usize) {
|
||||
let sealed = rows(&conn, "SELECT * FROM own_sealed_rev ORDER BY uid");
|
||||
(version, nodes, ops, sealed)
|
||||
};
|
||||
// A node made on this machine goes by its row's stand-in from schema 41
|
||||
// on, in its ops as everywhere else.
|
||||
let stand_ins: Vec<(String, String)> = nodes
|
||||
.iter()
|
||||
.filter(|(uid, rowid)| uid.starts_with("local~") && *uid != local_uid(*rowid))
|
||||
.map(|(uid, rowid)| (uid.clone(), local_uid(*rowid)))
|
||||
.collect();
|
||||
let nodes: Vec<(String, i64)> = nodes
|
||||
.into_iter()
|
||||
.map(
|
||||
|(uid, rowid)| match stand_ins.iter().any(|(old, _)| *old == uid) {
|
||||
true => (local_uid(rowid), rowid),
|
||||
false => (uid, rowid),
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
for value in ops.iter_mut().flatten() {
|
||||
if let rusqlite::types::Value::Text(text) = value {
|
||||
for (old, new) in &stand_ins {
|
||||
if text == old {
|
||||
*text = new.clone();
|
||||
} else {
|
||||
*text = text.replace(&format!("\"{old}\""), &format!("\"{new}\""));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let db = Db::open(path).unwrap();
|
||||
{
|
||||
let conn = rusqlite::Connection::open(path).unwrap();
|
||||
|
||||
+26
-32
@@ -3650,14 +3650,17 @@ impl Core {
|
||||
hold: bool,
|
||||
) -> Result<Node, Errno> {
|
||||
self.require_uid_writable(parent_uid)?;
|
||||
let uid = mint_local_uid();
|
||||
// The row the tree would give it anyway, now: the node goes by its
|
||||
// local id until it lands.
|
||||
let lid = self
|
||||
.db
|
||||
.add_local_row(&parent_uid.to_string(), name, is_dir, now_secs())
|
||||
.map_err(|e| {
|
||||
error!(%parent_uid, name, error = %e, "giving a local node its row failed");
|
||||
Errno::EIO
|
||||
})?;
|
||||
let uid = local_node_uid(lid);
|
||||
let node = local_node(uid.clone(), parent_uid.clone(), name.to_string(), is_dir);
|
||||
// The row the tree would give it anyway, now, so the op is queued with
|
||||
// the node's local id.
|
||||
self.db.lids_for(&[&node]).map_err(|e| {
|
||||
error!(%parent_uid, name, error = %e, "giving a local node its row failed");
|
||||
Errno::EIO
|
||||
})?;
|
||||
let op = PendingOp {
|
||||
id: 0,
|
||||
kind: if is_dir { OP_MKDIR } else { OP_CREATE }.to_string(),
|
||||
@@ -5752,21 +5755,12 @@ fn parse_node_uid(s: &str) -> Option<NodeUid> {
|
||||
parse_uid(s)
|
||||
}
|
||||
|
||||
/// Distinguishes placeholder uids minted by [`mint_local_uid`] within one run.
|
||||
static LOCAL_UID_SEQ: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
/// Invent a uid for a node created while offline, so it can be interned, listed
|
||||
/// and written to before the server has ever heard of it (offline.md Phase 3b).
|
||||
///
|
||||
/// Uniqueness only has to hold among this machine's undrained ops, so the clock
|
||||
/// (which separates runs) plus a counter (which separates nodes within a run) is
|
||||
/// enough without taking on a uuid dependency.
|
||||
fn mint_local_uid() -> NodeUid {
|
||||
let seq = LOCAL_UID_SEQ.fetch_add(1, Ordering::Relaxed);
|
||||
NodeUid::new(
|
||||
VolumeId::from(LOCAL_VOLUME),
|
||||
LinkId::from(format!("{}-{seq}", now_millis())),
|
||||
)
|
||||
/// The uid a node made on this machine goes by until it lands, so it can be
|
||||
/// interned, listed and written to before the server has ever heard of it
|
||||
/// (offline.md Phase 3b): its row's local id, as [`pdfs_core::db::local_uid`]
|
||||
/// has it.
|
||||
fn local_node_uid(lid: i64) -> NodeUid {
|
||||
NodeUid::new(VolumeId::from(LOCAL_VOLUME), LinkId::from(lid.to_string()))
|
||||
}
|
||||
|
||||
/// Whether this node exists only on this machine, so far. Such a uid is
|
||||
@@ -7022,24 +7016,24 @@ mod local_uid_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_minted_uid_is_recognisable_and_round_trips() {
|
||||
let uid = mint_local_uid();
|
||||
fn a_local_node_uid_is_recognisable_and_round_trips() {
|
||||
let uid = local_node_uid(42);
|
||||
assert!(is_local_uid(&uid));
|
||||
assert!(is_local_uid_str(&uid.to_string()));
|
||||
assert_eq!(uid.to_string(), pdfs_core::db::local_uid(42));
|
||||
|
||||
// It has to survive the trip through `pending_op.uid` as text, like any
|
||||
// other uid does.
|
||||
// other uid does, and give back the row it stands for.
|
||||
let parsed = parse_node_uid(&uid.to_string()).expect("parses back");
|
||||
assert_eq!(parsed, uid);
|
||||
assert_eq!(pdfs_core::db::local_lid(&uid.to_string()), Some(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minted_uids_are_distinct_within_a_run() {
|
||||
fn local_node_uids_are_distinct_per_row() {
|
||||
// Two files created in the same millisecond must not collide — the whole
|
||||
// queue is keyed by uid.
|
||||
let a = mint_local_uid();
|
||||
let b = mint_local_uid();
|
||||
assert_ne!(a, b);
|
||||
assert_ne!(local_node_uid(1), local_node_uid(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -7056,7 +7050,7 @@ mod local_uid_tests {
|
||||
#[test]
|
||||
fn a_placeholder_file_reports_itself_as_empty_and_unsealed() {
|
||||
let parent = NodeUid::new(VolumeId::from("vol1"), LinkId::from("dir"));
|
||||
let node = local_node(mint_local_uid(), parent.clone(), "notes.txt".into(), false);
|
||||
let node = local_node(local_node_uid(7), parent.clone(), "notes.txt".into(), false);
|
||||
|
||||
assert_eq!(node.name, "notes.txt");
|
||||
assert_eq!(node.parent_uid, Some(parent));
|
||||
@@ -7080,7 +7074,7 @@ mod local_uid_tests {
|
||||
#[test]
|
||||
fn a_placeholder_folder_is_a_folder() {
|
||||
let parent = NodeUid::new(VolumeId::from("vol1"), LinkId::from("root"));
|
||||
let node = local_node(mint_local_uid(), parent, "photos".into(), true);
|
||||
let node = local_node(local_node_uid(7), parent, "photos".into(), true);
|
||||
assert!(node.is_folder());
|
||||
}
|
||||
}
|
||||
@@ -8552,7 +8546,7 @@ mod tests {
|
||||
assert_before(truncate, "require_uid_writable", "create_scratch");
|
||||
|
||||
for (signature, first_side_effect) in [
|
||||
("fn queue_local_node(", "mint_local_uid"),
|
||||
("fn queue_local_node(", "add_local_row"),
|
||||
("fn queue_rename(", "queue_rename_authorized"),
|
||||
("fn queue_trash(", "replace_ops_with_trash"),
|
||||
] {
|
||||
|
||||
+3
-2
@@ -11,10 +11,11 @@ scratch (user data in `staging/` and `recovery/` is never touched by this).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
Schema: **40**. The queue table is rebuilt so an id is never given out twice; queued changes
|
||||
Schema: **41**. The queue table is rebuilt so an id is never given out twice; queued changes
|
||||
are kept. Queued changes get a column for the reason Drive refused them, and name their file
|
||||
and folder by local id as well as by Drive's id. Stored files and folders name their parent
|
||||
folder by local id too.
|
||||
folder by local id too. A file or folder made on this machine that Drive does not have yet goes
|
||||
by its local id, in the queue, the stored tree and pins alike.
|
||||
|
||||
### Added
|
||||
- **Sync issues.** A change Drive refuses because the storage is full, access was taken away,
|
||||
|
||||
@@ -37,7 +37,14 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase
|
||||
- [ ] Queries over children and subtrees by `parent_lid`, and no rewrite of the children's
|
||||
`parent_uid` when a folder lands (R3). Both go with the placeholders: until then the
|
||||
strings are kept in step.
|
||||
- [ ] No more placeholders: `nodes.uid` is `NULL` until a node lands.
|
||||
- [ ] No more placeholders: `nodes.uid` is `NULL` until a node lands. In four parts:
|
||||
- [x] 6a: a node made here goes by `local~<lid>`, a function of its row, instead of a
|
||||
minted `local~<ms>-<n>`; schema 41 rewrites the old placeholders (nodes, ops, pins).
|
||||
- [ ] 6b: ops by lid; a landing create rewrites no other op.
|
||||
- [ ] 6c: a local node's row has `uid NULL`; children and subtrees by `parent_lid`; pins
|
||||
by lid.
|
||||
- [ ] 6d: in memory, `pending`, `hidden` and `creating` by lid; `is_local_uid` checks
|
||||
become `remote_uid().is_none()`.
|
||||
- [ ] An executor that runs ops in dependency order, many at once.
|
||||
3. **Speed (§7)**: run independent ops in parallel, batch trashes and moves; measure 1,000 files
|
||||
and 100 folders on Wi-Fi against LAN (target: within 10 %).
|
||||
|
||||
Reference in new issue
Block a user