use std::path::PathBuf; use std::sync::{Arc, OnceLock, Mutex, RwLock}; use std::collections::HashMap; use crate::error::DBError; use crate::options; use crate::rpc::Permissions; use crate::storage::Storage; use crate::storage_sled::SledStorage; use crate::storage_trait::StorageBackend; // Key builders fn k_admin_next_id() -> &'static str { "admin:next_id" } fn k_admin_dbs() -> &'static str { "admin:dbs" } fn k_meta_db(id: u64) -> String { format!("meta:db:{}", id) } fn k_meta_db_keys(id: u64) -> String { format!("meta:db:{}:keys", id) } fn k_meta_db_enc(id: u64) -> String { format!("meta:db:{}:enc", id) } // Global cache of admin DB 0 handles per base_dir to avoid sled/reDB file-lock contention // and to correctly isolate different test instances with distinct directories. static ADMIN_STORAGES: OnceLock>>> = OnceLock::new(); // Global registry for data DB storages to avoid double-open across process. static DATA_STORAGES: OnceLock>>> = OnceLock::new(); static DATA_INIT_LOCK: Mutex<()> = Mutex::new(()); fn init_admin_storage( base_dir: &str, backend: options::BackendType, admin_secret: &str, ) -> Result, DBError> { let db_file = PathBuf::from(base_dir).join("0.db"); if let Some(parent_dir) = db_file.parent() { std::fs::create_dir_all(parent_dir).map_err(|e| { DBError(format!("Failed to create directory {}: {}", parent_dir.display(), e)) })?; } let storage: Arc = match backend { options::BackendType::Redb => Arc::new(Storage::new(&db_file, true, Some(admin_secret))?), options::BackendType::Sled => Arc::new(SledStorage::new(&db_file, true, Some(admin_secret))?), options::BackendType::Tantivy => { return Err(DBError("Admin DB 0 cannot use Tantivy backend".to_string())) } }; Ok(storage) } // Get or initialize a cached handle to admin DB 0 per base_dir (thread-safe, no double-open race) pub fn open_admin_storage( base_dir: &str, backend: options::BackendType, admin_secret: &str, ) -> Result, DBError> { let map = ADMIN_STORAGES.get_or_init(|| RwLock::new(HashMap::new())); // Fast path if let Some(st) = map.read().unwrap().get(base_dir) { return Ok(st.clone()); } // Slow path with write lock { let mut w = map.write().unwrap(); if let Some(st) = w.get(base_dir) { return Ok(st.clone()); } // Detect existing 0.db backend by filesystem, if present. let admin_path = PathBuf::from(base_dir).join("0.db"); let detected = if admin_path.exists() { if admin_path.is_file() { Some(options::BackendType::Redb) } else if admin_path.is_dir() { Some(options::BackendType::Sled) } else { None } } else { None }; let effective_backend = match detected { Some(d) if d != backend => { eprintln!( "warning: Admin DB 0 at {} appears to be {:?}, but process default is {:?}. Using detected backend.", admin_path.display(), d, backend ); d } Some(d) => d, None => backend, // First boot: use requested backend to initialize 0.db }; let st = init_admin_storage(base_dir, effective_backend, admin_secret)?; w.insert(base_dir.to_string(), st.clone()); Ok(st) } } // Ensure admin structures exist in encrypted DB 0 pub fn ensure_bootstrap( base_dir: &str, backend: options::BackendType, admin_secret: &str, ) -> Result<(), DBError> { let admin = open_admin_storage(base_dir, backend, admin_secret)?; // Initialize next id if missing if !admin.exists(k_admin_next_id())? { admin.set(k_admin_next_id().to_string(), "1".to_string())?; } // admin:dbs is a hash; it's fine if it doesn't exist (hlen -> 0) Ok(()) } // Get or initialize a shared handle to a data DB (> 0), avoiding double-open across subsystems pub fn open_data_storage( base_dir: &str, backend: options::BackendType, admin_secret: &str, id: u64, ) -> Result, DBError> { if id == 0 { return open_admin_storage(base_dir, backend, admin_secret); } // Validate existence in admin metadata if !db_exists(base_dir, backend.clone(), admin_secret, id)? { return Err(DBError(format!( "Cannot open database instance {}, as that database instance does not exist.", id ))); } let map = DATA_STORAGES.get_or_init(|| RwLock::new(HashMap::new())); // Fast path if let Some(st) = map.read().unwrap().get(&id) { return Ok(st.clone()); } // Slow path with init lock let _guard = DATA_INIT_LOCK.lock().unwrap(); if let Some(st) = map.read().unwrap().get(&id) { return Ok(st.clone()); } // Resolve effective backend for this db id: // 1) Try admin meta "backend" field // 2) If missing, sniff filesystem (file => Redb, dir => Sled), then persist into admin meta // 3) Fallback to requested 'backend' (startup default) if nothing else is known let meta_backend = get_database_backend(base_dir, backend.clone(), admin_secret, id).ok().flatten(); let db_path = PathBuf::from(base_dir).join(format!("{}.db", id)); let sniffed_backend = if db_path.exists() { if db_path.is_file() { Some(options::BackendType::Redb) } else if db_path.is_dir() { Some(options::BackendType::Sled) } else { None } } else { None }; let effective_backend = meta_backend.clone().or(sniffed_backend).unwrap_or(backend.clone()); // If we had to sniff (i.e., meta missing), persist it for future robustness if meta_backend.is_none() { let _ = set_database_backend(base_dir, backend.clone(), admin_secret, id, effective_backend.clone()); } // Warn if caller-provided backend differs from effective if effective_backend != backend { eprintln!( "notice: Database {} backend resolved to {:?} (caller requested {:?}). Using resolved backend.", id, effective_backend, backend ); } // Determine per-db encryption (from admin meta) let enc = get_enc_key(base_dir, backend.clone(), admin_secret, id)?; let should_encrypt = enc.is_some(); // Build database file path and ensure parent dir exists let db_file = PathBuf::from(base_dir).join(format!("{}.db", id)); if let Some(parent_dir) = db_file.parent() { std::fs::create_dir_all(parent_dir).map_err(|e| { DBError(format!("Failed to create directory {}: {}", parent_dir.display(), e)) })?; } // Open storage using the effective backend let storage: Arc = match effective_backend { options::BackendType::Redb => Arc::new(Storage::new(&db_file, should_encrypt, enc.as_deref())?), options::BackendType::Sled => Arc::new(SledStorage::new(&db_file, should_encrypt, enc.as_deref())?), options::BackendType::Tantivy => { return Err(DBError("Tantivy backend has no KV storage; use FT.* commands only".to_string())) } }; // Publish to registry map.write().unwrap().insert(id, storage.clone()); Ok(storage) } // Allocate the next DB id and persist new pointer pub fn allocate_next_id( base_dir: &str, backend: options::BackendType, admin_secret: &str, ) -> Result { let admin = open_admin_storage(base_dir, backend, admin_secret)?; let cur = admin .get(k_admin_next_id())? .unwrap_or_else(|| "1".to_string()); let id: u64 = cur.parse().unwrap_or(1); let next = id.checked_add(1).ok_or_else(|| DBError("next_id overflow".into()))?; admin.set(k_admin_next_id().to_string(), next.to_string())?; // Register into admin:dbs set/hash let _ = admin.hset(k_admin_dbs(), vec![(id.to_string(), "1".to_string())])?; // Default meta for the new db: public true let meta_key = k_meta_db(id); let _ = admin.hset(&meta_key, vec![("public".to_string(), "true".to_string())])?; Ok(id) } // Check existence of a db id in admin:dbs pub fn db_exists( base_dir: &str, backend: options::BackendType, admin_secret: &str, id: u64, ) -> Result { let admin = open_admin_storage(base_dir, backend, admin_secret)?; Ok(admin.hexists(k_admin_dbs(), &id.to_string())?) } // Get per-db encryption key, if any pub fn get_enc_key( base_dir: &str, backend: options::BackendType, admin_secret: &str, id: u64, ) -> Result, DBError> { let admin = open_admin_storage(base_dir, backend, admin_secret)?; admin.get(&k_meta_db_enc(id)) } // Set per-db encryption key (called during create) pub fn set_enc_key( base_dir: &str, backend: options::BackendType, admin_secret: &str, id: u64, key: &str, ) -> Result<(), DBError> { let admin = open_admin_storage(base_dir, backend, admin_secret)?; admin.set(k_meta_db_enc(id), key.to_string()) } // Set database public flag pub fn set_database_public( base_dir: &str, backend: options::BackendType, admin_secret: &str, id: u64, public: bool, ) -> Result<(), DBError> { let admin = open_admin_storage(base_dir, backend, admin_secret)?; let mk = k_meta_db(id); let _ = admin.hset(&mk, vec![("public".to_string(), public.to_string())])?; Ok(()) } // Persist per-db backend type in admin metadata (module-scope) pub fn set_database_backend( base_dir: &str, backend: options::BackendType, admin_secret: &str, id: u64, db_backend: options::BackendType, ) -> Result<(), DBError> { let admin = open_admin_storage(base_dir, backend, admin_secret)?; let mk = k_meta_db(id); let val = match db_backend { options::BackendType::Redb => "Redb", options::BackendType::Sled => "Sled", options::BackendType::Tantivy => "Tantivy", }; let _ = admin.hset(&mk, vec![("backend".to_string(), val.to_string())])?; Ok(()) } pub fn get_database_backend( base_dir: &str, backend: options::BackendType, admin_secret: &str, id: u64, ) -> Result, DBError> { let admin = open_admin_storage(base_dir, backend, admin_secret)?; let mk = k_meta_db(id); match admin.hget(&mk, "backend")? { Some(s) if s == "Redb" => Ok(Some(options::BackendType::Redb)), Some(s) if s == "Sled" => Ok(Some(options::BackendType::Sled)), Some(s) if s == "Tantivy" => Ok(Some(options::BackendType::Tantivy)), _ => Ok(None), } } // Set database name pub fn set_database_name( base_dir: &str, backend: options::BackendType, admin_secret: &str, id: u64, name: &str, ) -> Result<(), DBError> { let admin = open_admin_storage(base_dir, backend, admin_secret)?; let mk = k_meta_db(id); let _ = admin.hset(&mk, vec![("name".to_string(), name.to_string())])?; Ok(()) } // Get database name pub fn get_database_name( base_dir: &str, backend: options::BackendType, admin_secret: &str, id: u64, ) -> Result, DBError> { let admin = open_admin_storage(base_dir, backend, admin_secret)?; let mk = k_meta_db(id); admin.hget(&mk, "name") } // Internal: load public flag; default to true when meta missing fn load_public( admin: &Arc, id: u64, ) -> Result { let mk = k_meta_db(id); match admin.hget(&mk, "public")? { Some(v) => Ok(v == "true"), None => Ok(true), } } // Add access key for db (value format: "Read:ts" or "ReadWrite:ts") pub fn add_access_key( base_dir: &str, backend: options::BackendType, admin_secret: &str, id: u64, key_plain: &str, perms: Permissions, ) -> Result<(), DBError> { let admin = open_admin_storage(base_dir, backend, admin_secret)?; let hash = crate::rpc::hash_key(key_plain); let v = match perms { Permissions::Read => format!("Read:{}", now_secs()), Permissions::ReadWrite => format!("ReadWrite:{}", now_secs()), }; let _ = admin.hset(&k_meta_db_keys(id), vec![(hash, v)])?; Ok(()) } // Delete access key by hash pub fn delete_access_key( base_dir: &str, backend: options::BackendType, admin_secret: &str, id: u64, key_hash: &str, ) -> Result { let admin = open_admin_storage(base_dir, backend, admin_secret)?; let n = admin.hdel(&k_meta_db_keys(id), vec![key_hash.to_string()])?; Ok(n > 0) } // List access keys, returning (hash, perms, created_at_secs) pub fn list_access_keys( base_dir: &str, backend: options::BackendType, admin_secret: &str, id: u64, ) -> Result, DBError> { let admin = open_admin_storage(base_dir, backend, admin_secret)?; let pairs = admin.hgetall(&k_meta_db_keys(id))?; let mut out = Vec::new(); for (hash, val) in pairs { let (perm, ts) = parse_perm_value(&val); out.push((hash, perm, ts)); } Ok(out) } // Verify access permission for db id with optional key // Returns: // - Ok(Some(Permissions)) when access is allowed // - Ok(None) when not allowed or db missing (caller can distinguish by calling db_exists) pub fn verify_access( base_dir: &str, backend: options::BackendType, admin_secret: &str, id: u64, key_opt: Option<&str>, ) -> Result, DBError> { // Admin DB 0: require exact admin_secret if id == 0 { if let Some(k) = key_opt { if k == admin_secret { return Ok(Some(Permissions::ReadWrite)); } } return Ok(None); } let admin = open_admin_storage(base_dir, backend, admin_secret)?; if !admin.hexists(k_admin_dbs(), &id.to_string())? { return Ok(None); } // Public? if load_public(&admin, id)? { return Ok(Some(Permissions::ReadWrite)); } // Private: require key and verify if let Some(k) = key_opt { let hash = crate::rpc::hash_key(k); if let Some(v) = admin.hget(&k_meta_db_keys(id), &hash)? { let (perm, _ts) = parse_perm_value(&v); return Ok(Some(perm)); } } Ok(None) } // Enumerate all db ids pub fn list_dbs( base_dir: &str, backend: options::BackendType, admin_secret: &str, ) -> Result, DBError> { let admin = open_admin_storage(base_dir, backend, admin_secret)?; let ids = admin.hkeys(k_admin_dbs())?; let mut out = Vec::new(); for s in ids { if let Ok(v) = s.parse() { out.push(v); } } Ok(out) } // Helper: parse permission value "Read:ts" or "ReadWrite:ts" fn parse_perm_value(v: &str) -> (Permissions, u64) { let mut parts = v.split(':'); let p = parts.next().unwrap_or("Read"); let ts = parts .next() .and_then(|s| s.parse().ok()) .unwrap_or(0u64); let perm = match p { "ReadWrite" => Permissions::ReadWrite, _ => Permissions::Read, }; (perm, ts) } fn now_secs() -> u64 { use std::time::{SystemTime, UNIX_EPOCH}; SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() }