Add local DB resource registration
This commit is contained in:
parent
21920b51b5
commit
4ec50d7473
14 changed files with 313 additions and 8 deletions
|
|
@ -419,9 +419,9 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
|
|||
module: "pipe".to_owned(),
|
||||
command: format!("{command:?}"),
|
||||
},
|
||||
Command::Db { command } => ControlRequest::ModuleStub {
|
||||
module: "db".to_owned(),
|
||||
command: format!("{command:?}"),
|
||||
Command::Db { command } => match command {
|
||||
DbCommand::Add { name, path } => ControlRequest::DbAdd { name, path },
|
||||
DbCommand::Status { name } => ControlRequest::DbStatus { name },
|
||||
},
|
||||
Command::Document { command } => ControlRequest::ModuleStub {
|
||||
module: "document".to_owned(),
|
||||
|
|
@ -744,6 +744,28 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
|
|||
ControlResponse::SshRevocationExported { out, count } => {
|
||||
println!("exported {count} ssh revocations to {}", out.display());
|
||||
}
|
||||
ControlResponse::DbAdded { db } => {
|
||||
println!("registered db: {}", db.name);
|
||||
println!("id: {}", db.id);
|
||||
println!("resource: {}", db.resource);
|
||||
println!("path: {}", db.path);
|
||||
println!("sync_status: {}", db.sync_status);
|
||||
}
|
||||
ControlResponse::DbStatus { db } => {
|
||||
println!("db: {}", db.name);
|
||||
println!("id: {}", db.id);
|
||||
println!("resource: {}", db.resource);
|
||||
println!("path: {}", db.path);
|
||||
println!("path_exists: {}", db.path_exists);
|
||||
println!(
|
||||
"size_bytes: {}",
|
||||
db.size_bytes
|
||||
.map(|size| size.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_owned())
|
||||
);
|
||||
println!("schema_metadata: {}", db.schema_metadata);
|
||||
println!("sync_status: {}", db.sync_status);
|
||||
}
|
||||
ControlResponse::NotImplemented { module, command } => {
|
||||
println!("{module} {command}: not implemented yet");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ serde.workspace = true
|
|||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
geth-auth = { path = "../geth-auth" }
|
||||
geth-db = { path = "../geth-db" }
|
||||
geth-keychain = { path = "../geth-keychain" }
|
||||
geth-resource = { path = "../geth-resource" }
|
||||
geth-ssh-identity = { path = "../geth-ssh-identity" }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use geth_auth::{AuthExplanation, AuthOp};
|
||||
use geth_db::DbResource;
|
||||
use geth_keychain::KeychainOp;
|
||||
use geth_resource::ResourceDescriptor;
|
||||
use geth_ssh_identity::{
|
||||
|
|
@ -90,6 +91,13 @@ pub enum ControlRequest {
|
|||
SshRevocationExport {
|
||||
out: PathBuf,
|
||||
},
|
||||
DbAdd {
|
||||
name: String,
|
||||
path: PathBuf,
|
||||
},
|
||||
DbStatus {
|
||||
name: String,
|
||||
},
|
||||
ModuleStub {
|
||||
module: String,
|
||||
command: String,
|
||||
|
|
@ -169,6 +177,12 @@ pub enum ControlResponse {
|
|||
out: PathBuf,
|
||||
count: usize,
|
||||
},
|
||||
DbAdded {
|
||||
db: DbResource,
|
||||
},
|
||||
DbStatus {
|
||||
db: DbResource,
|
||||
},
|
||||
NotImplemented {
|
||||
module: String,
|
||||
command: String,
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ license.workspace = true
|
|||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
geth-types = { path = "../geth-types" }
|
||||
|
|
|
|||
|
|
@ -5,11 +5,47 @@ use serde::{Deserialize, Serialize};
|
|||
pub struct DbResource {
|
||||
pub id: DbId,
|
||||
pub resource: ResourceId,
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub path_exists: bool,
|
||||
pub size_bytes: Option<u64>,
|
||||
pub schema_metadata: String,
|
||||
pub sync_status: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum DbError {
|
||||
#[error("invalid db resource name: {0}")]
|
||||
InvalidName(String),
|
||||
}
|
||||
|
||||
pub fn validate_db_name(name: &str) -> Result<(), DbError> {
|
||||
if name.is_empty()
|
||||
|| !name
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
|
||||
{
|
||||
return Err(DbError::InvalidName(name.to_owned()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn crsqlite_sync_roadmap() -> &'static str {
|
||||
"future db sync reads crsql_changes, exchanges changes over Iroh, and applies through crsql_changes"
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn db_name_validation_rejects_paths_and_empty_names() {
|
||||
assert!(validate_db_name("notes").is_ok());
|
||||
assert!(validate_db_name("notes.v1").is_ok());
|
||||
assert!(validate_db_name("").is_err());
|
||||
assert!(validate_db_name("../notes").is_err());
|
||||
assert!(validate_db_name("notes/main").is_err());
|
||||
assert!(validate_db_name("notes main").is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ geth-cas = { path = "../geth-cas" }
|
|||
geth-config = { path = "../geth-config" }
|
||||
geth-control = { path = "../geth-control" }
|
||||
geth-crypto = { path = "../geth-crypto" }
|
||||
geth-db = { path = "../geth-db" }
|
||||
geth-iroh = { path = "../geth-iroh" }
|
||||
geth-keychain = { path = "../geth-keychain" }
|
||||
geth-resource = { path = "../geth-resource" }
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use geth_control::{
|
|||
StatusResponse,
|
||||
};
|
||||
use geth_crypto::AgentKey;
|
||||
use geth_db::DbResource;
|
||||
use geth_iroh::{EndpointStatus, GethIrohConfig, GethIrohEndpoint, GethRelayMode};
|
||||
use geth_keychain::{KeychainOp, KeychainOpKind};
|
||||
use geth_resource::ResourceDescriptor;
|
||||
|
|
@ -17,7 +18,7 @@ use geth_ssh_identity::{
|
|||
certificate_id, revocation_id, ssh_public_key_fingerprint,
|
||||
};
|
||||
use geth_store::{
|
||||
Store, StoredAuthOp, StoredKeychainOp, StoredResource, StoredSshCertRequest,
|
||||
Store, StoredAuthOp, StoredDbResource, StoredKeychainOp, StoredResource, StoredSshCertRequest,
|
||||
StoredSshCertificate, StoredSshRevocation,
|
||||
};
|
||||
use geth_types::{
|
||||
|
|
@ -46,6 +47,12 @@ pub enum NodeError {
|
|||
Io(#[from] std::io::Error),
|
||||
#[error("invalid resource kind: {0}")]
|
||||
InvalidResourceKind(String),
|
||||
#[error("invalid db resource name: {0}")]
|
||||
InvalidDbName(String),
|
||||
#[error("db path does not exist or is not a file: {0}")]
|
||||
InvalidDbPath(String),
|
||||
#[error("db resource not found: {0}")]
|
||||
DbNotFound(String),
|
||||
#[error("invalid ssh certificate kind: {0}")]
|
||||
InvalidSshCertKind(String),
|
||||
#[error("invalid ssh certificate request status: {0}")]
|
||||
|
|
@ -550,6 +557,41 @@ pub fn handle_request(
|
|||
count: revocations.len(),
|
||||
})
|
||||
}
|
||||
ControlRequest::DbAdd { name, path } => {
|
||||
geth_db::validate_db_name(&name).map_err(|_| NodeError::InvalidDbName(name.clone()))?;
|
||||
if !path.is_file() {
|
||||
return Err(NodeError::InvalidDbPath(path.display().to_string()));
|
||||
}
|
||||
let path = std::fs::canonicalize(path)?;
|
||||
let resource_id = format!("resource:db:{name}");
|
||||
let db_id = format!("db:{name}");
|
||||
let resource = StoredResource {
|
||||
resource_id: resource_id.clone(),
|
||||
kind: ResourceKind::Db.to_string(),
|
||||
name: name.clone(),
|
||||
status: "active".to_owned(),
|
||||
};
|
||||
store.insert_resource(&resource)?;
|
||||
let stored = StoredDbResource {
|
||||
db_id,
|
||||
resource_id,
|
||||
name,
|
||||
path: path.display().to_string(),
|
||||
};
|
||||
store.insert_db_resource(&stored)?;
|
||||
Ok(ControlResponse::DbAdded {
|
||||
db: db_resource_from_stored(&stored)?,
|
||||
})
|
||||
}
|
||||
ControlRequest::DbStatus { name } => {
|
||||
geth_db::validate_db_name(&name).map_err(|_| NodeError::InvalidDbName(name.clone()))?;
|
||||
let stored = store
|
||||
.get_db_resource_by_name(&name)?
|
||||
.ok_or_else(|| NodeError::DbNotFound(name.clone()))?;
|
||||
Ok(ControlResponse::DbStatus {
|
||||
db: db_resource_from_stored(&stored)?,
|
||||
})
|
||||
}
|
||||
ControlRequest::ModuleStub { module, command } => {
|
||||
Ok(ControlResponse::NotImplemented { module, command })
|
||||
}
|
||||
|
|
@ -568,6 +610,21 @@ fn stored_resource_to_descriptor(stored: StoredResource) -> Result<ResourceDescr
|
|||
))
|
||||
}
|
||||
|
||||
fn db_resource_from_stored(stored: &StoredDbResource) -> Result<DbResource, NodeError> {
|
||||
let path = Path::new(&stored.path);
|
||||
let metadata = path.metadata().ok();
|
||||
Ok(DbResource {
|
||||
id: stored.db_id.clone().into(),
|
||||
resource: stored.resource_id.clone().into(),
|
||||
name: stored.name.clone(),
|
||||
path: stored.path.clone(),
|
||||
path_exists: metadata.as_ref().is_some_and(std::fs::Metadata::is_file),
|
||||
size_bytes: metadata.map(|metadata| metadata.len()),
|
||||
schema_metadata: "not-inspected-until-crsqlite-integration".to_owned(),
|
||||
sync_status: "local-only".to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
fn store_auth_op(store: &Store, op: &AuthOp) -> Result<(), NodeError> {
|
||||
store.insert_auth_op(&StoredAuthOp {
|
||||
op_id: op.id.to_string(),
|
||||
|
|
|
|||
|
|
@ -222,6 +222,59 @@ impl Store {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_resource_by_kind_name(
|
||||
&self,
|
||||
kind: &str,
|
||||
name: &str,
|
||||
) -> Result<Option<StoredResource>, StoreError> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT resource_id, kind, name, status FROM resources WHERE kind = ?1 AND name = ?2",
|
||||
)?;
|
||||
let mut rows = stmt.query(params![kind, name])?;
|
||||
if let Some(row) = rows.next()? {
|
||||
Ok(Some(StoredResource {
|
||||
resource_id: row.get(0)?,
|
||||
kind: row.get(1)?,
|
||||
name: row.get(2)?,
|
||||
status: row.get(3)?,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_db_resource(&self, db: &StoredDbResource) -> Result<(), StoreError> {
|
||||
self.conn.execute(
|
||||
r#"INSERT OR REPLACE INTO db_resources(db_id, resource_id, path)
|
||||
VALUES (?1, ?2, ?3)"#,
|
||||
params![db.db_id, db.resource_id, db.path],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_db_resource_by_name(
|
||||
&self,
|
||||
name: &str,
|
||||
) -> Result<Option<StoredDbResource>, StoreError> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
r#"SELECT db.db_id, db.resource_id, resources.name, db.path
|
||||
FROM db_resources db
|
||||
JOIN resources ON resources.resource_id = db.resource_id
|
||||
WHERE resources.kind = 'db' AND resources.name = ?1"#,
|
||||
)?;
|
||||
let mut rows = stmt.query(params![name])?;
|
||||
if let Some(row) = rows.next()? {
|
||||
Ok(Some(StoredDbResource {
|
||||
db_id: row.get(0)?,
|
||||
resource_id: row.get(1)?,
|
||||
name: row.get(2)?,
|
||||
path: row.get(3)?,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_cas_object(
|
||||
&self,
|
||||
hash: &str,
|
||||
|
|
@ -561,6 +614,14 @@ pub struct StoredResource {
|
|||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct StoredDbResource {
|
||||
pub db_id: String,
|
||||
pub resource_id: String,
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CasObject {
|
||||
pub hash: String,
|
||||
|
|
@ -788,4 +849,38 @@ mod tests {
|
|||
assert!(store.list_cas_objects().expect("objects").is_empty());
|
||||
assert!(store.list_cas_pins().expect("pins").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn db_resource_roundtrip_by_name() {
|
||||
let store = Store::open_memory().expect("open");
|
||||
let resource = StoredResource {
|
||||
resource_id: "resource:db:notes".to_owned(),
|
||||
kind: "db".to_owned(),
|
||||
name: "notes".to_owned(),
|
||||
status: "active".to_owned(),
|
||||
};
|
||||
store.insert_resource(&resource).expect("insert resource");
|
||||
store
|
||||
.insert_db_resource(&StoredDbResource {
|
||||
db_id: "db:notes".to_owned(),
|
||||
resource_id: resource.resource_id.clone(),
|
||||
name: resource.name.clone(),
|
||||
path: "/tmp/notes.sqlite".to_owned(),
|
||||
})
|
||||
.expect("insert db");
|
||||
|
||||
assert_eq!(
|
||||
store
|
||||
.get_resource_by_kind_name("db", "notes")
|
||||
.expect("get resource"),
|
||||
Some(resource)
|
||||
);
|
||||
assert_eq!(
|
||||
store
|
||||
.get_db_resource_by_name("notes")
|
||||
.expect("get db")
|
||||
.map(|db| db.path),
|
||||
Some("/tmp/notes.sqlite".to_owned())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -387,6 +387,73 @@ fn keychain_init_and_status_use_local_keychain_log() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn db_add_and_status_register_local_db_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 db_path = home.path().join("notes.sqlite");
|
||||
std::fs::write(&db_path, b"sqlite placeholder").expect("write db");
|
||||
|
||||
let response = geth_node::handle_request(
|
||||
&node,
|
||||
geth_control::ControlRequest::DbAdd {
|
||||
name: "notes".to_owned(),
|
||||
path: db_path.clone(),
|
||||
},
|
||||
)
|
||||
.expect("add db");
|
||||
match response {
|
||||
geth_control::ControlResponse::DbAdded { db } => {
|
||||
assert_eq!(db.name, "notes");
|
||||
assert_eq!(db.sync_status, "local-only");
|
||||
assert!(db.path_exists);
|
||||
assert_eq!(db.size_bytes, Some(18));
|
||||
}
|
||||
other => panic!("unexpected response: {other:?}"),
|
||||
}
|
||||
|
||||
let response = geth_node::handle_request(
|
||||
&node,
|
||||
geth_control::ControlRequest::DbStatus {
|
||||
name: "notes".to_owned(),
|
||||
},
|
||||
)
|
||||
.expect("db status");
|
||||
match response {
|
||||
geth_control::ControlResponse::DbStatus { db } => {
|
||||
assert_eq!(db.name, "notes");
|
||||
assert!(db.path.ends_with("notes.sqlite"));
|
||||
assert_eq!(
|
||||
db.schema_metadata,
|
||||
"not-inspected-until-crsqlite-integration"
|
||||
);
|
||||
}
|
||||
other => panic!("unexpected response: {other:?}"),
|
||||
}
|
||||
|
||||
assert!(
|
||||
geth_node::handle_request(
|
||||
&node,
|
||||
geth_control::ControlRequest::DbAdd {
|
||||
name: "../bad".to_owned(),
|
||||
path: db_path,
|
||||
},
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
geth_node::handle_request(
|
||||
&node,
|
||||
geth_control::ControlRequest::DbAdd {
|
||||
name: "missing".to_owned(),
|
||||
path: home.path().join("missing.sqlite"),
|
||||
},
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_cert_request_approval_and_revocation_export_use_local_state() {
|
||||
let home = tempfile::tempdir().expect("tempdir");
|
||||
|
|
|
|||
Loading…
Reference in a new issue