Live-sync authorized file roots
This commit is contained in:
parent
1caaedda90
commit
bab13cc0a1
5 changed files with 128 additions and 7 deletions
|
|
@ -141,9 +141,10 @@ Roadmap items should be actionable and checkable:
|
|||
to the working tree. `geth cas root sync <node-id> <name>` can pull authorized
|
||||
remote tree metadata and CAS tree bytes, recording a peer-qualified remote
|
||||
root with path `remote:<node>:<name>` without applying files or overwriting
|
||||
same-named local roots. Durable local file-conflict records can be listed and
|
||||
resolved manually; automatic cross-node conflict detection and file application
|
||||
are still roadmap work.
|
||||
same-named local roots. Background live-sync refreshes authorized remote file
|
||||
roots from sync-status `cas-tree:<name>` watermarks. Durable local
|
||||
file-conflict records can be listed and resolved manually; automatic
|
||||
cross-node conflict detection and file application are still roadmap work.
|
||||
- DB resources can be registered locally and report local-only status plus a
|
||||
read-only SQLite schema summary/hash and `crsql_changes` metadata when
|
||||
present. The DB crate and daemon can extract typed read-only `crsql_changes`
|
||||
|
|
|
|||
|
|
@ -177,6 +177,9 @@ Before probing individual modules, the daemon asks the peer for an authorized
|
|||
sync-status summary over Iroh. The peer only returns stream watermarks for
|
||||
resources where the caller already has the matching capability, letting the
|
||||
local daemon skip unchanged or unauthorized streams.
|
||||
File roots advertise `cas-tree:<name>` watermarks when the caller has
|
||||
`cas.fetch`; background live-sync imports updated tree metadata and CAS tree
|
||||
bytes into peer-qualified remote roots without writing files.
|
||||
Named KV stores participate in the same live-sync loop once they exist locally:
|
||||
manual `geth kv sync <node-id> <name>` and background ticks require `kv.read`
|
||||
on the remote `resource:kv:<name>` and import only remote entries that are not
|
||||
|
|
|
|||
|
|
@ -917,6 +917,7 @@ async fn cas_root_sync_from_peer(
|
|||
}
|
||||
let store = Store::open(&node.paths.metadata_db())?;
|
||||
let mut tree_bytes_imported = false;
|
||||
let mut imported_high_water = None;
|
||||
let root = root
|
||||
.map(|remote_root| {
|
||||
let stored_name = remote_file_root_name(&node_id, &remote_root.name);
|
||||
|
|
@ -960,9 +961,18 @@ async fn cas_root_sync_from_peer(
|
|||
updated_at_ms: remote_root.updated_at_ms,
|
||||
};
|
||||
store.upsert_file_root(&stored)?;
|
||||
imported_high_water = Some(remote_root.updated_at_ms);
|
||||
Ok(file_root_from_stored(&stored))
|
||||
})
|
||||
.transpose()?;
|
||||
if let Some(high_water_ms) = imported_high_water {
|
||||
store_live_sync_cursor(
|
||||
&store,
|
||||
peer_node,
|
||||
&format!("cas-tree:{response_name}"),
|
||||
high_water_ms,
|
||||
)?;
|
||||
}
|
||||
Ok(ControlResponse::CasRootSynced {
|
||||
peer_node_id: node_id,
|
||||
peer_agent_id: agent_id,
|
||||
|
|
@ -1844,6 +1854,18 @@ fn sync_watermarks_for_peer(
|
|||
}
|
||||
}
|
||||
}
|
||||
for root in store.list_file_roots()? {
|
||||
if root.path.starts_with("remote:") {
|
||||
continue;
|
||||
}
|
||||
let resource = format!("resource:cas-tree:{}", root.name);
|
||||
if can_sync_resource(store, &peer, &resource, "cas.fetch")? {
|
||||
watermarks.push(SyncWatermark {
|
||||
stream: format!("cas-tree:{}", root.name),
|
||||
high_water: root.updated_at_ms,
|
||||
});
|
||||
}
|
||||
}
|
||||
watermarks.sort_by(|left, right| left.stream.cmp(&right.stream));
|
||||
Ok(watermarks)
|
||||
}
|
||||
|
|
@ -2176,6 +2198,22 @@ async fn run_live_sync_once(node: &LocalNode) -> Result<(), NodeError> {
|
|||
tracing::debug!(peer = %peer.peer_id, %error, "SSH revocation live sync failed");
|
||||
}
|
||||
}
|
||||
if let Some(remote_watermarks) = remote_watermarks.as_ref() {
|
||||
for stream in remote_watermarks
|
||||
.keys()
|
||||
.filter(|stream| stream.starts_with("cas-tree:"))
|
||||
{
|
||||
if should_live_sync_stream(&store, &peer.peer_id, stream, Some(remote_watermarks))?
|
||||
{
|
||||
let name = stream.trim_start_matches("cas-tree:");
|
||||
if let Err(error) =
|
||||
cas_root_sync_from_peer(node, &peer.peer_id, name, None).await
|
||||
{
|
||||
tracing::debug!(peer = %peer.peer_id, root = %name, %error, "file-root live sync failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for kv in &kv_stores {
|
||||
let stream = format!("kv:{}", kv.name);
|
||||
if should_live_sync_stream(&store, &peer.peer_id, &stream, remote_watermarks.as_ref())?
|
||||
|
|
@ -5358,9 +5396,32 @@ mod tests {
|
|||
path: db_path.display().to_string(),
|
||||
})
|
||||
.expect("insert db");
|
||||
store
|
||||
.upsert_file_root(&StoredFileRoot {
|
||||
root_id: "file-root:shared".to_owned(),
|
||||
resource_id: "resource:cas-tree:shared".to_owned(),
|
||||
name: "shared".to_owned(),
|
||||
path: dir.path().join("shared").display().to_string(),
|
||||
latest_tree_hash: Some("abc".to_owned()),
|
||||
latest_tree_json: None,
|
||||
updated_at_ms: 123,
|
||||
})
|
||||
.expect("insert file root");
|
||||
store
|
||||
.upsert_file_root(&StoredFileRoot {
|
||||
root_id: "file-root:remote:node:right:shared".to_owned(),
|
||||
resource_id: "resource:cas-tree:shared".to_owned(),
|
||||
name: "remote-node-right-shared".to_owned(),
|
||||
path: "remote:node:right:shared".to_owned(),
|
||||
latest_tree_hash: Some("def".to_owned()),
|
||||
latest_tree_json: None,
|
||||
updated_at_ms: 456,
|
||||
})
|
||||
.expect("insert imported remote file root");
|
||||
|
||||
grant_test_capability(&store, "node:left", "resource:kv:prefs", "kv.read");
|
||||
grant_test_capability(&store, "node:left", "resource:db:notes", "db.sync");
|
||||
grant_test_capability(&store, "node:left", "resource:cas-tree:shared", "cas.fetch");
|
||||
|
||||
let watermarks = sync_watermarks_for_peer(&store, "node:left").expect("watermarks");
|
||||
|
||||
|
|
@ -5372,6 +5433,15 @@ mod tests {
|
|||
stream: "db:notes".to_owned(),
|
||||
high_water: 7,
|
||||
}));
|
||||
assert!(watermarks.contains(&SyncWatermark {
|
||||
stream: "cas-tree:shared".to_owned(),
|
||||
high_water: 123,
|
||||
}));
|
||||
assert!(
|
||||
!watermarks
|
||||
.iter()
|
||||
.any(|watermark| watermark.stream == "cas-tree:remote-node-right-shared")
|
||||
);
|
||||
assert!(
|
||||
!watermarks
|
||||
.iter()
|
||||
|
|
@ -5475,7 +5545,7 @@ mod tests {
|
|||
&right,
|
||||
ControlRequest::CasRootAdd {
|
||||
name: "shared".to_owned(),
|
||||
path: right_file_root,
|
||||
path: right_file_root.clone(),
|
||||
},
|
||||
)
|
||||
.expect("right file root add");
|
||||
|
|
@ -6588,6 +6658,19 @@ mod tests {
|
|||
},
|
||||
)
|
||||
.expect("right live document set");
|
||||
std::fs::write(right_file_root.join("live.txt"), b"live file root sync")
|
||||
.expect("write live file");
|
||||
let second_tree_hash = match handle_request(
|
||||
&right,
|
||||
ControlRequest::CasRootScan {
|
||||
name: "shared".to_owned(),
|
||||
},
|
||||
)
|
||||
.expect("right live file root scan")
|
||||
{
|
||||
ControlResponse::CasRootScanned { scan } => scan.tree.hash,
|
||||
other => panic!("unexpected right live file root scan response: {other:?}"),
|
||||
};
|
||||
insert_mock_crsqlite_change(&right_db_path, 8, "live");
|
||||
|
||||
run_live_sync_once(&left)
|
||||
|
|
@ -6622,6 +6705,34 @@ mod tests {
|
|||
.map(|document| document.state_json),
|
||||
Some(r#"{"title":"live"}"#.to_owned())
|
||||
);
|
||||
let live_remote_root = left_store
|
||||
.list_file_roots()
|
||||
.expect("list live-synced file roots")
|
||||
.into_iter()
|
||||
.find(|root| root.path == format!("remote:{}:shared", right.node_id))
|
||||
.expect("live-synced remote root");
|
||||
assert_eq!(
|
||||
live_remote_root.latest_tree_hash.as_deref(),
|
||||
Some(second_tree_hash.as_str())
|
||||
);
|
||||
assert!(
|
||||
LocalCas::new(left.paths.cas_dir())
|
||||
.has(&second_tree_hash)
|
||||
.expect("left has live-synced tree object")
|
||||
);
|
||||
let file_root_cursor = left_store
|
||||
.get_module_state(&live_sync_cursor_key(
|
||||
right_card.node_id.as_str(),
|
||||
"cas-tree:shared",
|
||||
))
|
||||
.expect("get live-synced file root cursor")
|
||||
.expect("file root cursor exists");
|
||||
assert_eq!(
|
||||
serde_json::from_str::<LiveSyncCursor>(&file_root_cursor.state_json)
|
||||
.expect("parse file root cursor")
|
||||
.cursor_ms,
|
||||
live_remote_root.updated_at_ms
|
||||
);
|
||||
let db_cursor = left_store
|
||||
.get_module_state(&live_sync_cursor_key(
|
||||
right_card.node_id.as_str(),
|
||||
|
|
|
|||
|
|
@ -131,8 +131,9 @@ scans are local metadata only and never overwrite the working tree. A peer can
|
|||
pull authorized remote file-root tree metadata with `geth cas root sync <node>
|
||||
<name>` when it has `cas.fetch` on `resource:cas-tree:<name>`; sync imports the
|
||||
remote CAS tree bytes and records a peer-qualified remote root whose path is
|
||||
`remote:<node>:<name>` without applying files. The daemon also has durable local
|
||||
file-conflict records with
|
||||
`remote:<node>:<name>` without applying files. File roots also participate in
|
||||
the daemon background live-sync loop through authorized `cas-tree:<name>`
|
||||
watermarks. The daemon also has durable local file-conflict records with
|
||||
explicit resolution choices; future cross-node file sync will create those
|
||||
records automatically instead of silently applying ambiguous remote changes.
|
||||
|
||||
|
|
@ -235,7 +236,10 @@ authorization testing: non-owner subjects must hold `ssh_cert.*` capabilities on
|
|||
`resource:ssh:revocations` before requests, approval/import/read operations, or
|
||||
revocation publish/read/import operations
|
||||
are accepted. The live-sync loop first asks for authorized stream watermarks and
|
||||
skips module pulls whose remote high-water value has not advanced.
|
||||
skips module pulls whose remote high-water value has not advanced. File-root
|
||||
live-sync uses only sync-status-advertised `cas-tree:<name>` streams, because
|
||||
the local node otherwise does not know which roots the peer is willing to
|
||||
expose.
|
||||
|
||||
## Keychain, Auth, And Secrets
|
||||
|
||||
|
|
|
|||
|
|
@ -517,6 +517,8 @@ and future group key evolution.
|
|||
- `[x]` Sync imports CAS tree bytes and records a peer-qualified remote root
|
||||
with path `remote:<node>:<name>` without writing files into the working tree
|
||||
or overwriting same-named local roots.
|
||||
- `[x]` Background live-sync imports updated authorized file-root trees from
|
||||
sync-status `cas-tree:<name>` watermarks and stores per-peer cursors.
|
||||
- `[ ]` Future completion applies file-root sync safely with conflict
|
||||
detection and explicit resolution.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue