Add local CAS pin metadata
This commit is contained in:
parent
99f6dee26f
commit
00471bcec0
9 changed files with 169 additions and 16 deletions
|
|
@ -116,6 +116,8 @@ Roadmap items should be actionable and checkable:
|
|||
enforcement are still roadmap work.
|
||||
- The daemon persists local keychain init/admin-key ops and reduces them for
|
||||
`keychain status`. SSH signature capture/verification is still roadmap work.
|
||||
- Local CAS supports pin/unpin metadata, surfaced through `cas list`. Cache
|
||||
cleanup and eviction policy are still roadmap work.
|
||||
- Signed peer-card LAN discovery payloads, peer auth over Iroh, cr-sqlite,
|
||||
iroh-docs, iroh-gossip, iroh-blobs, Automerge sync, real auth enforcement,
|
||||
OpenSSH KRL generation, and Keyhive/BeeKEM-style authorization are future
|
||||
|
|
|
|||
|
|
@ -79,7 +79,8 @@ The bootstrap implementation provides:
|
|||
- `geth auth explain <subject> <resource> <capability>`
|
||||
- `geth auth grant <subject> <resource> <capability> [--grant-id <id>]`
|
||||
- `geth auth revoke <resource> <grant-id>`
|
||||
- local filesystem CAS commands: `add`, `get`, `hash`, `has`, `list`
|
||||
- local filesystem CAS commands: `add`, `get`, `hash`, `has`, `pin`, `unpin`,
|
||||
`list`
|
||||
- SSH certificate flow metadata:
|
||||
- `geth ssh cert request --public-key <path> --principal <name>`
|
||||
- `geth ssh cert requests`
|
||||
|
|
|
|||
|
|
@ -179,6 +179,12 @@ pub enum CasCommand {
|
|||
Has {
|
||||
hash: String,
|
||||
},
|
||||
Pin {
|
||||
hash: String,
|
||||
},
|
||||
Unpin {
|
||||
hash: String,
|
||||
},
|
||||
List,
|
||||
}
|
||||
|
||||
|
|
@ -392,6 +398,8 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
|||
},
|
||||
CasCommand::Hash { path } => ControlRequest::CasHash { path },
|
||||
CasCommand::Has { hash } => ControlRequest::CasHas { hash: hash.into() },
|
||||
CasCommand::Pin { hash } => ControlRequest::CasPin { hash: hash.into() },
|
||||
CasCommand::Unpin { hash } => ControlRequest::CasUnpin { hash: hash.into() },
|
||||
CasCommand::List => ControlRequest::CasList,
|
||||
},
|
||||
Command::Kv { command } => ControlRequest::ModuleStub {
|
||||
|
|
@ -585,9 +593,13 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
|||
}
|
||||
ControlResponse::CasHash { hash } => println!("{hash}"),
|
||||
ControlResponse::CasHas { hash, present } => println!("{hash}: {present}"),
|
||||
ControlResponse::CasPinned { hash, pinned } => {
|
||||
println!("{hash}: pinned={pinned}");
|
||||
}
|
||||
ControlResponse::CasList { blobs } => {
|
||||
for blob in blobs {
|
||||
println!("{}\t{} bytes", blob.hash, blob.size_bytes);
|
||||
let pin = if blob.pinned { "pinned" } else { "unpinned" };
|
||||
println!("{}\t{} bytes\t{}", blob.hash, blob.size_bytes, pin);
|
||||
}
|
||||
}
|
||||
ControlResponse::KeychainStatus(status) => {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,12 @@ pub enum ControlRequest {
|
|||
CasHas {
|
||||
hash: BlobHash,
|
||||
},
|
||||
CasPin {
|
||||
hash: BlobHash,
|
||||
},
|
||||
CasUnpin {
|
||||
hash: BlobHash,
|
||||
},
|
||||
CasList,
|
||||
KeychainInit {
|
||||
admin_key_path: Option<PathBuf>,
|
||||
|
|
@ -114,6 +120,10 @@ pub enum ControlResponse {
|
|||
hash: BlobHash,
|
||||
present: bool,
|
||||
},
|
||||
CasPinned {
|
||||
hash: BlobHash,
|
||||
pinned: bool,
|
||||
},
|
||||
CasList {
|
||||
blobs: Vec<CasBlob>,
|
||||
},
|
||||
|
|
@ -193,6 +203,7 @@ pub struct KeychainStatusResponse {
|
|||
pub struct CasBlob {
|
||||
pub hash: BlobHash,
|
||||
pub size_bytes: u64,
|
||||
pub pinned: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
|
|
|||
|
|
@ -225,18 +225,38 @@ pub fn handle_request(
|
|||
let present = cas.has(&hash)?;
|
||||
Ok(ControlResponse::CasHas { hash, present })
|
||||
}
|
||||
ControlRequest::CasPin { hash } => {
|
||||
let cas = LocalCas::new(node.paths.cas_dir());
|
||||
if !cas.has(&hash)? {
|
||||
return Err(NodeError::Cas(geth_cas::CasError::NotFound(
|
||||
hash.to_string(),
|
||||
)));
|
||||
}
|
||||
store.pin_cas_object(hash.as_str())?;
|
||||
Ok(ControlResponse::CasPinned { hash, pinned: true })
|
||||
}
|
||||
ControlRequest::CasUnpin { hash } => {
|
||||
geth_cas::validate_hash(&hash)?;
|
||||
store.unpin_cas_object(hash.as_str())?;
|
||||
Ok(ControlResponse::CasPinned {
|
||||
hash,
|
||||
pinned: false,
|
||||
})
|
||||
}
|
||||
ControlRequest::CasList => {
|
||||
let cas = LocalCas::new(node.paths.cas_dir());
|
||||
Ok(ControlResponse::CasList {
|
||||
blobs: cas
|
||||
.list()?
|
||||
.into_iter()
|
||||
.map(|blob| CasBlob {
|
||||
let blobs = cas
|
||||
.list()?
|
||||
.into_iter()
|
||||
.map(|blob| {
|
||||
Ok(CasBlob {
|
||||
pinned: store.is_cas_object_pinned(blob.hash.as_str())?,
|
||||
hash: blob.hash,
|
||||
size_bytes: blob.size_bytes,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, NodeError>>()?;
|
||||
Ok(ControlResponse::CasList { blobs })
|
||||
}
|
||||
ControlRequest::KeychainInit { admin_key_path } => {
|
||||
let mut ops = Vec::new();
|
||||
|
|
|
|||
|
|
@ -250,6 +250,43 @@ impl Store {
|
|||
.map_err(StoreError::from)
|
||||
}
|
||||
|
||||
pub fn pin_cas_object(&self, hash: &str) -> Result<(), StoreError> {
|
||||
self.conn.execute(
|
||||
"INSERT OR REPLACE INTO cas_pins(hash, pinned_at_ms) VALUES (?1, ?2)",
|
||||
params![hash, now_ms()],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unpin_cas_object(&self, hash: &str) -> Result<(), StoreError> {
|
||||
self.conn
|
||||
.execute("DELETE FROM cas_pins WHERE hash = ?1", params![hash])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_cas_object_pinned(&self, hash: &str) -> Result<bool, StoreError> {
|
||||
let count: i64 = self.conn.query_row(
|
||||
"SELECT COUNT(*) FROM cas_pins WHERE hash = ?1",
|
||||
params![hash],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
pub fn list_cas_pins(&self) -> Result<Vec<CasPin>, StoreError> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT hash, pinned_at_ms FROM cas_pins ORDER BY pinned_at_ms, hash")?;
|
||||
let rows = stmt.query_map([], |row| {
|
||||
Ok(CasPin {
|
||||
hash: row.get(0)?,
|
||||
pinned_at_ms: row.get(1)?,
|
||||
})
|
||||
})?;
|
||||
rows.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(StoreError::from)
|
||||
}
|
||||
|
||||
pub fn upsert_peer_card(&self, peer_card: &StoredPeerCard) -> Result<(), StoreError> {
|
||||
self.conn.execute(
|
||||
"INSERT OR REPLACE INTO peer_cards(peer_id, card_json, updated_at_ms) VALUES (?1, ?2, ?3)",
|
||||
|
|
@ -523,6 +560,12 @@ pub struct CasObject {
|
|||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CasPin {
|
||||
pub hash: String,
|
||||
pub pinned_at_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StoredPeerCard {
|
||||
pub peer_id: String,
|
||||
|
|
@ -708,4 +751,18 @@ mod tests {
|
|||
vec![first, second]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cas_pins_roundtrip() {
|
||||
let store = Store::open_memory().expect("open");
|
||||
let hash = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
|
||||
assert!(!store.is_cas_object_pinned(hash).expect("unpinned"));
|
||||
store.pin_cas_object(hash).expect("pin");
|
||||
assert!(store.is_cas_object_pinned(hash).expect("pinned"));
|
||||
assert_eq!(store.list_cas_pins().expect("pins").len(), 1);
|
||||
store.unpin_cas_object(hash).expect("unpin");
|
||||
assert!(!store.is_cas_object_pinned(hash).expect("unpinned again"));
|
||||
assert!(store.list_cas_pins().expect("pins").is_empty());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,6 +122,52 @@ fn initialized_node_can_roundtrip_cas_blob() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cas_pin_unpin_updates_local_pin_metadata() {
|
||||
let home = tempfile::tempdir().expect("tempdir");
|
||||
let paths = geth_config::GethPaths::from_home(home.path());
|
||||
let node = geth_node::init_node(&paths).expect("init node");
|
||||
let blob_path = home.path().join("blob.txt");
|
||||
std::fs::write(&blob_path, b"pinned bytes").expect("write blob");
|
||||
|
||||
let response = geth_node::handle_request(
|
||||
&node,
|
||||
geth_control::ControlRequest::CasAdd { path: blob_path },
|
||||
)
|
||||
.expect("add cas blob");
|
||||
let hash = match response {
|
||||
geth_control::ControlResponse::CasAdded { hash, .. } => hash,
|
||||
other => panic!("unexpected response: {other:?}"),
|
||||
};
|
||||
|
||||
geth_node::handle_request(
|
||||
&node,
|
||||
geth_control::ControlRequest::CasPin { hash: hash.clone() },
|
||||
)
|
||||
.expect("pin blob");
|
||||
let response = geth_node::handle_request(&node, geth_control::ControlRequest::CasList)
|
||||
.expect("list blobs");
|
||||
match response {
|
||||
geth_control::ControlResponse::CasList { blobs } => {
|
||||
assert_eq!(blobs.len(), 1);
|
||||
assert!(blobs[0].pinned);
|
||||
}
|
||||
other => panic!("unexpected response: {other:?}"),
|
||||
}
|
||||
|
||||
geth_node::handle_request(&node, geth_control::ControlRequest::CasUnpin { hash })
|
||||
.expect("unpin blob");
|
||||
let response = geth_node::handle_request(&node, geth_control::ControlRequest::CasList)
|
||||
.expect("list blobs");
|
||||
match response {
|
||||
geth_control::ControlResponse::CasList { blobs } => {
|
||||
assert_eq!(blobs.len(), 1);
|
||||
assert!(!blobs[0].pinned);
|
||||
}
|
||||
other => panic!("unexpected response: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_explain_distinguishes_discovered_peer_candidates() {
|
||||
let home = tempfile::tempdir().expect("tempdir");
|
||||
|
|
|
|||
|
|
@ -88,8 +88,9 @@ Resource kinds:
|
|||
## Module Overview
|
||||
|
||||
`geth-cas` is implemented locally first using BLAKE3 hashes and filesystem blob
|
||||
storage. Iroh-blobs, providers, encrypted blobs, manifests, and file sync trees
|
||||
are future work.
|
||||
storage. Local pin/unpin metadata is tracked in SQLite and surfaced in
|
||||
`cas list`. Iroh-blobs, providers, encrypted blobs, cache cleanup, manifests,
|
||||
and file sync trees are future work.
|
||||
|
||||
`geth-kv`, `geth-db`, `geth-document`, `geth-pubsub`, `geth-pipe`, and
|
||||
`geth-ssh-proxy` currently define types, command shape, and roadmap stubs.
|
||||
|
|
|
|||
|
|
@ -200,11 +200,14 @@ authorization and durable-state boundaries clear.
|
|||
- Provider tracking is recorded locally.
|
||||
- Local add/get/hash/has/list behavior remains backward compatible.
|
||||
|
||||
- `[ ]` CAS pin and cache policy.
|
||||
- `[~]` CAS pin and cache policy.
|
||||
Acceptance criteria:
|
||||
- Pinned blobs are retained across cache cleanup.
|
||||
- Unpinned cached blobs can be evicted by policy.
|
||||
- Tests cover pin, unpin, list, and attempted eviction of pinned data.
|
||||
- `[x]` `geth cas pin <hash>` persists local pin metadata.
|
||||
- `[x]` `geth cas unpin <hash>` removes local pin metadata.
|
||||
- `[x]` `geth cas list` reports pinned versus unpinned blobs.
|
||||
- `[ ]` Pinned blobs are retained across cache cleanup.
|
||||
- `[ ]` Unpinned cached blobs can be evicted by policy.
|
||||
- `[ ]` Tests cover attempted eviction of pinned data.
|
||||
|
||||
- `[ ]` Encrypted private blobs.
|
||||
Acceptance criteria:
|
||||
|
|
|
|||
Loading…
Reference in a new issue