fixed DEL showing wrong deletion amount + AGE LIST now returns a list of managed keys names without nested arrays or labels
This commit is contained in:
33
src/age.rs
33
src/age.rs
@@ -19,6 +19,7 @@ use age::x25519;
|
|||||||
use ed25519_dalek::{Signature, Signer, Verifier, SigningKey, VerifyingKey};
|
use ed25519_dalek::{Signature, Signer, Verifier, SigningKey, VerifyingKey};
|
||||||
|
|
||||||
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
|
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use crate::protocol::Protocol;
|
use crate::protocol::Protocol;
|
||||||
use crate::server::Server;
|
use crate::server::Server;
|
||||||
@@ -276,33 +277,31 @@ pub async fn cmd_age_verify_name(server: &Server, name: &str, message: &str, sig
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cmd_age_list(server: &Server) -> Protocol {
|
pub async fn cmd_age_list(server: &Server) -> Protocol {
|
||||||
// Returns 4 arrays: ["encpub", <names...>], ["encpriv", ...], ["signpub", ...], ["signpriv", ...]
|
// Return a flat, deduplicated, sorted list of managed key names (no labels)
|
||||||
let st = match server.current_storage() { Ok(s) => s, Err(e) => return Protocol::err(&e.0) };
|
let st = match server.current_storage() { Ok(s) => s, Err(e) => return Protocol::err(&e.0) };
|
||||||
|
|
||||||
let pull = |pat: &str, prefix: &str| -> Result<Vec<String>, DBError> {
|
let pull = |pat: &str, prefix: &str| -> Result<Vec<String>, DBError> {
|
||||||
let keys = st.keys(pat)?;
|
let keys = st.keys(pat)?;
|
||||||
let mut names: Vec<String> = keys.into_iter()
|
let mut names: Vec<String> = keys
|
||||||
|
.into_iter()
|
||||||
.filter_map(|k| k.strip_prefix(prefix).map(|x| x.to_string()))
|
.filter_map(|k| k.strip_prefix(prefix).map(|x| x.to_string()))
|
||||||
.collect();
|
.collect();
|
||||||
names.sort();
|
names.sort();
|
||||||
Ok(names)
|
Ok(names)
|
||||||
};
|
};
|
||||||
|
|
||||||
let encpub = match pull("age:key:*", "age:key:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
|
let encpub = match pull("age:key:*", "age:key:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
|
||||||
let encpriv = match pull("age:privkey:*", "age:privkey:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
|
let encpriv = match pull("age:privkey:*", "age:privkey:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
|
||||||
let signpub = match pull("age:signpub:*", "age:signpub:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
|
let signpub = match pull("age:signpub:*", "age:signpub:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
|
||||||
let signpriv= match pull("age:signpriv:*", "age:signpriv:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
|
let signpriv = match pull("age:signpriv:*", "age:signpriv:") { Ok(v) => v, Err(e)=> return Protocol::err(&e.0) };
|
||||||
|
|
||||||
let to_arr = |label: &str, v: Vec<String>| {
|
let mut set: HashSet<String> = HashSet::new();
|
||||||
let mut out = vec![Protocol::BulkString(label.to_string())];
|
for n in encpub.into_iter().chain(encpriv).chain(signpub).chain(signpriv) {
|
||||||
out.push(Protocol::Array(v.into_iter().map(Protocol::BulkString).collect()));
|
set.insert(n);
|
||||||
Protocol::Array(out)
|
}
|
||||||
};
|
|
||||||
|
|
||||||
Protocol::Array(vec![
|
let mut names: Vec<String> = set.into_iter().collect();
|
||||||
to_arr("encpub", encpub),
|
names.sort();
|
||||||
to_arr("encpriv", encpriv),
|
|
||||||
to_arr("signpub", signpub),
|
Protocol::Array(names.into_iter().map(Protocol::BulkString).collect())
|
||||||
to_arr("signpriv", signpriv),
|
|
||||||
])
|
|
||||||
}
|
}
|
12
src/cmd.rs
12
src/cmd.rs
@@ -1210,8 +1210,13 @@ async fn del_cmd(server: &Server, k: &str) -> Result<Protocol, DBError> {
|
|||||||
if !server.has_write_permission() {
|
if !server.has_write_permission() {
|
||||||
return Ok(Protocol::err("ERR write permission denied"));
|
return Ok(Protocol::err("ERR write permission denied"));
|
||||||
}
|
}
|
||||||
server.current_storage()?.del(k.to_string())?;
|
let storage = server.current_storage()?;
|
||||||
Ok(Protocol::SimpleString("1".to_string()))
|
if storage.exists(k)? {
|
||||||
|
storage.del(k.to_string())?;
|
||||||
|
Ok(Protocol::SimpleString("1".to_string()))
|
||||||
|
} else {
|
||||||
|
Ok(Protocol::SimpleString("0".to_string()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_ex_cmd(
|
async fn set_ex_cmd(
|
||||||
@@ -1322,6 +1327,9 @@ async fn mset_cmd(server: &Server, pairs: &[(String, String)]) -> Result<Protoco
|
|||||||
|
|
||||||
// DEL with multiple keys: return count of keys actually deleted
|
// DEL with multiple keys: return count of keys actually deleted
|
||||||
async fn del_multi_cmd(server: &Server, keys: &[String]) -> Result<Protocol, DBError> {
|
async fn del_multi_cmd(server: &Server, keys: &[String]) -> Result<Protocol, DBError> {
|
||||||
|
if !server.has_write_permission() {
|
||||||
|
return Ok(Protocol::err("ERR write permission denied"));
|
||||||
|
}
|
||||||
let storage = server.current_storage()?;
|
let storage = server.current_storage()?;
|
||||||
let mut deleted = 0i64;
|
let mut deleted = 0i64;
|
||||||
for k in keys {
|
for k in keys {
|
||||||
|
160
src/rpc.rs
160
src/rpc.rs
@@ -48,20 +48,7 @@ pub enum Permissions {
|
|||||||
ReadWrite,
|
ReadWrite,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Access key information
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct AccessKey {
|
|
||||||
pub hash: String,
|
|
||||||
pub permissions: Permissions,
|
|
||||||
pub created_at: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Database metadata containing access keys
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct DatabaseMeta {
|
|
||||||
pub public: bool,
|
|
||||||
pub keys: HashMap<String, AccessKey>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Access key information returned by RPC
|
/// Access key information returned by RPC
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -133,14 +120,8 @@ pub struct RpcServerImpl {
|
|||||||
base_dir: String,
|
base_dir: String,
|
||||||
/// Managed database servers
|
/// Managed database servers
|
||||||
servers: Arc<RwLock<HashMap<u64, Arc<Server>>>>,
|
servers: Arc<RwLock<HashMap<u64, Arc<Server>>>>,
|
||||||
/// Next unencrypted database ID to assign
|
|
||||||
next_unencrypted_id: Arc<RwLock<u64>>,
|
|
||||||
/// Next encrypted database ID to assign
|
|
||||||
next_encrypted_id: Arc<RwLock<u64>>,
|
|
||||||
/// Default backend type
|
/// Default backend type
|
||||||
backend: crate::options::BackendType,
|
backend: crate::options::BackendType,
|
||||||
/// Encryption keys for databases
|
|
||||||
encryption_keys: Arc<RwLock<HashMap<u64, Option<String>>>>,
|
|
||||||
/// Admin secret used to encrypt DB 0 and authorize admin access
|
/// Admin secret used to encrypt DB 0 and authorize admin access
|
||||||
admin_secret: String,
|
admin_secret: String,
|
||||||
}
|
}
|
||||||
@@ -151,10 +132,7 @@ impl RpcServerImpl {
|
|||||||
Self {
|
Self {
|
||||||
base_dir,
|
base_dir,
|
||||||
servers: Arc::new(RwLock::new(HashMap::new())),
|
servers: Arc::new(RwLock::new(HashMap::new())),
|
||||||
next_unencrypted_id: Arc::new(RwLock::new(0)),
|
|
||||||
next_encrypted_id: Arc::new(RwLock::new(10)),
|
|
||||||
backend,
|
backend,
|
||||||
encryption_keys: Arc::new(RwLock::new(HashMap::new())),
|
|
||||||
admin_secret,
|
admin_secret,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,124 +190,10 @@ impl RpcServerImpl {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the next available database ID
|
|
||||||
async fn get_next_db_id(&self, is_encrypted: bool) -> u64 {
|
|
||||||
if is_encrypted {
|
|
||||||
let mut id = self.next_encrypted_id.write().await;
|
|
||||||
let current_id = *id;
|
|
||||||
*id += 1;
|
|
||||||
current_id
|
|
||||||
} else {
|
|
||||||
let mut id = self.next_unencrypted_id.write().await;
|
|
||||||
let current_id = *id;
|
|
||||||
*id += 1;
|
|
||||||
current_id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load database metadata from file (static version)
|
|
||||||
pub async fn load_meta_static(base_dir: &str, db_id: u64) -> Result<DatabaseMeta, jsonrpsee::types::ErrorObjectOwned> {
|
|
||||||
let meta_path = std::path::PathBuf::from(base_dir).join(format!("{}_meta.json", db_id));
|
|
||||||
|
|
||||||
// If meta file doesn't exist, create and persist default
|
|
||||||
if !meta_path.exists() {
|
|
||||||
let default_meta = DatabaseMeta {
|
|
||||||
public: true,
|
|
||||||
keys: HashMap::new(),
|
|
||||||
};
|
|
||||||
// Persist default metadata to disk
|
|
||||||
Self::save_meta_static(base_dir, db_id, &default_meta).await?;
|
|
||||||
return Ok(default_meta);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read file as UTF-8 JSON
|
|
||||||
let json_str = std::fs::read_to_string(&meta_path)
|
|
||||||
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(
|
|
||||||
-32000,
|
|
||||||
format!("Failed to read meta file: {}", e),
|
|
||||||
None::<()>
|
|
||||||
))?;
|
|
||||||
|
|
||||||
serde_json::from_str(&json_str)
|
|
||||||
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(
|
|
||||||
-32000,
|
|
||||||
format!("Failed to parse meta JSON: {}", e),
|
|
||||||
None::<()>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load database metadata from file
|
|
||||||
async fn load_meta(&self, db_id: u64) -> Result<DatabaseMeta, jsonrpsee::types::ErrorObjectOwned> {
|
|
||||||
let meta_path = std::path::PathBuf::from(&self.base_dir).join(format!("{}_meta.json", db_id));
|
|
||||||
|
|
||||||
// If meta file doesn't exist, create and persist default
|
|
||||||
if !meta_path.exists() {
|
|
||||||
let default_meta = DatabaseMeta {
|
|
||||||
public: true,
|
|
||||||
keys: HashMap::new(),
|
|
||||||
};
|
|
||||||
self.save_meta(db_id, &default_meta).await?;
|
|
||||||
return Ok(default_meta);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read file as UTF-8 JSON (meta files are always plain JSON)
|
|
||||||
let json_str = std::fs::read_to_string(&meta_path)
|
|
||||||
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(
|
|
||||||
-32000,
|
|
||||||
format!("Failed to read meta file: {}", e),
|
|
||||||
None::<()>
|
|
||||||
))?;
|
|
||||||
|
|
||||||
serde_json::from_str(&json_str)
|
|
||||||
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(
|
|
||||||
-32000,
|
|
||||||
format!("Failed to parse meta JSON: {}", e),
|
|
||||||
None::<()>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save database metadata to file (static version)
|
|
||||||
pub async fn save_meta_static(base_dir: &str, db_id: u64, meta: &DatabaseMeta) -> Result<(), jsonrpsee::types::ErrorObjectOwned> {
|
|
||||||
let meta_path = std::path::PathBuf::from(base_dir).join(format!("{}_meta.json", db_id));
|
|
||||||
|
|
||||||
let json_str = serde_json::to_string_pretty(meta)
|
|
||||||
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(
|
|
||||||
-32000,
|
|
||||||
format!("Failed to serialize meta: {}", e),
|
|
||||||
None::<()>
|
|
||||||
))?;
|
|
||||||
|
|
||||||
std::fs::write(&meta_path, json_str)
|
|
||||||
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(
|
|
||||||
-32000,
|
|
||||||
format!("Failed to write meta file: {}", e),
|
|
||||||
None::<()>
|
|
||||||
))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save database metadata to file
|
|
||||||
async fn save_meta(&self, db_id: u64, meta: &DatabaseMeta) -> Result<(), jsonrpsee::types::ErrorObjectOwned> {
|
|
||||||
let meta_path = std::path::PathBuf::from(&self.base_dir).join(format!("{}_meta.json", db_id));
|
|
||||||
|
|
||||||
let json_str = serde_json::to_string_pretty(meta)
|
|
||||||
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(
|
|
||||||
-32000,
|
|
||||||
format!("Failed to serialize meta: {}", e),
|
|
||||||
None::<()>
|
|
||||||
))?;
|
|
||||||
|
|
||||||
// Meta files are always stored as plain JSON (even when data DB is encrypted)
|
|
||||||
std::fs::write(&meta_path, json_str)
|
|
||||||
.map_err(|e| jsonrpsee::types::ErrorObjectOwned::owned(
|
|
||||||
-32000,
|
|
||||||
format!("Failed to write meta file: {}", e),
|
|
||||||
None::<()>
|
|
||||||
))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build database file path for given server/db_id
|
/// Build database file path for given server/db_id
|
||||||
fn db_file_path(&self, server: &Server, db_id: u64) -> std::path::PathBuf {
|
fn db_file_path(&self, server: &Server, db_id: u64) -> std::path::PathBuf {
|
||||||
@@ -374,29 +238,21 @@ impl RpcServerImpl {
|
|||||||
(created, last_access)
|
(created, last_access)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compose a DatabaseInfo by probing storage, metadata and filesystem
|
/// Compose a DatabaseInfo by probing storage and filesystem, with admin meta for access key count
|
||||||
async fn build_database_info(&self, db_id: u64, server: &Server) -> DatabaseInfo {
|
async fn build_database_info(&self, db_id: u64, server: &Server) -> DatabaseInfo {
|
||||||
// Probe storage to determine encryption state
|
// Probe storage to determine encryption state
|
||||||
let storage = server.current_storage().ok();
|
let storage = server.current_storage().ok();
|
||||||
let encrypted = storage.as_ref().map(|s| s.is_encrypted()).unwrap_or(server.option.encrypt);
|
let encrypted = storage.as_ref().map(|s| s.is_encrypted()).unwrap_or(server.option.encrypt);
|
||||||
|
|
||||||
// Load meta to get access key count
|
// Access key count via admin DB 0
|
||||||
let meta = Self::load_meta_static(&self.base_dir, db_id).await.unwrap_or(DatabaseMeta {
|
let key_count = admin_meta::list_access_keys(&self.base_dir, self.backend.clone(), &self.admin_secret, db_id)
|
||||||
public: true,
|
.map(|v| v.len() as u64)
|
||||||
keys: HashMap::new(),
|
.ok();
|
||||||
});
|
|
||||||
let key_count = Some(meta.keys.len() as u64);
|
|
||||||
|
|
||||||
// Compute size on disk and timestamps
|
// Compute size on disk and timestamps from the DB file path
|
||||||
let db_path = self.db_file_path(server, db_id);
|
let db_path = self.db_file_path(server, db_id);
|
||||||
let size_on_disk = self.compute_size_on_disk(&db_path);
|
let size_on_disk = self.compute_size_on_disk(&db_path);
|
||||||
|
let (created_at, last_access) = Self::get_file_times_secs(&db_path);
|
||||||
let meta_path = std::path::PathBuf::from(&self.base_dir).join(format!("{}_meta.json", db_id));
|
|
||||||
let (created_at, last_access) = if meta_path.exists() {
|
|
||||||
Self::get_file_times_secs(&meta_path)
|
|
||||||
} else {
|
|
||||||
Self::get_file_times_secs(&db_path)
|
|
||||||
};
|
|
||||||
|
|
||||||
let backend = match server.option.backend {
|
let backend = match server.option.backend {
|
||||||
crate::options::BackendType::Redb => BackendType::Redb,
|
crate::options::BackendType::Redb => BackendType::Redb,
|
||||||
@@ -468,7 +324,7 @@ impl RpcServer for RpcServerImpl {
|
|||||||
Ok(db_id)
|
Ok(db_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_encryption(&self, db_id: u64, _encryption_key: String) -> RpcResult<bool> {
|
async fn set_encryption(&self, _db_id: u64, _encryption_key: String) -> RpcResult<bool> {
|
||||||
// For now, return false as encryption can only be set during creation
|
// For now, return false as encryption can only be set during creation
|
||||||
let _servers = self.servers.read().await;
|
let _servers = self.servers.read().await;
|
||||||
// TODO: Implement encryption setting for existing databases
|
// TODO: Implement encryption setting for existing databases
|
||||||
|
@@ -290,7 +290,11 @@ async fn test_02_strings_and_expiry() {
|
|||||||
|
|
||||||
let ex0 = send_cmd(&mut s, &["EXISTS", "user:1"]).await;
|
let ex0 = send_cmd(&mut s, &["EXISTS", "user:1"]).await;
|
||||||
assert_contains(&ex0, "0", "EXISTS after DEL");
|
assert_contains(&ex0, "0", "EXISTS after DEL");
|
||||||
|
|
||||||
|
// DEL non-existent should return 0
|
||||||
|
let del0 = send_cmd(&mut s, &["DEL", "user:1"]).await;
|
||||||
|
assert_contains(&del0, "0", "DEL user:1 when not exists -> 0");
|
||||||
|
|
||||||
// INCR behavior
|
// INCR behavior
|
||||||
let i1 = send_cmd(&mut s, &["INCR", "count"]).await;
|
let i1 = send_cmd(&mut s, &["INCR", "count"]).await;
|
||||||
assert_contains(&i1, "1", "INCR new key -> 1");
|
assert_contains(&i1, "1", "INCR new key -> 1");
|
||||||
@@ -602,7 +606,7 @@ async fn test_08_age_persistent_named_suite() {
|
|||||||
|
|
||||||
// AGE LIST
|
// AGE LIST
|
||||||
let lst = send_cmd(&mut s, &["AGE", "LIST"]).await;
|
let lst = send_cmd(&mut s, &["AGE", "LIST"]).await;
|
||||||
assert_contains(&lst, "encpub", "AGE LIST label encpub");
|
// After flattening, LIST returns a flat array of managed key names
|
||||||
assert_contains(&lst, "app1", "AGE LIST includes app1");
|
assert_contains(&lst, "app1", "AGE LIST includes app1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user