feat(client >> storage): implemented a persistent key-value storage system
This commit is contained in:
parent
a3b0263f97
commit
08cf73a613
|
@ -1158,6 +1158,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"reqwest",
|
||||
"serde",
|
||||
"sqlx",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"uuid",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
test.db
|
|
@ -6,6 +6,7 @@ edition = "2021"
|
|||
[dependencies]
|
||||
reqwest = { version = "0.12.2", features = ["json"] }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio"] }
|
||||
thiserror = "1.0.58"
|
||||
tokio = { version = "1.37.0", features = ["full"] }
|
||||
uuid = "1.8.0"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
mod error;
|
||||
mod session;
|
||||
pub mod error;
|
||||
pub mod session;
|
||||
pub mod storage;
|
||||
|
||||
pub use session::Session;
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
pub mod standard;
|
||||
|
||||
#[derive(Debug, thiserror::Error, Clone)]
|
||||
pub enum StorageError {
|
||||
#[error("There is already an entry with the id '{0}'")]
|
||||
IdCollision(String),
|
||||
|
||||
#[error("The id '{0}' could not be found in the storage system")]
|
||||
IdNotFound(String),
|
||||
|
||||
#[error("Unknown error in storage system: {0}")]
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
/// This trait is implemented by storage systems. It provides the interface for a simple key-value
|
||||
/// lookup storage. The data should be stored persistent.
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait Storage: Sized {
|
||||
/// Save binary data in the storage. The data is identified by `id` and can be retrieved again
|
||||
/// using [Storage::pull].
|
||||
async fn push(&mut self, id: &str, data: Vec<u8>) -> Result<(), StorageError>;
|
||||
|
||||
/// Reads binary data identified by `id` from the storage. Returns an error if `id` is unknown.
|
||||
async fn pull(&mut self, id: &str) -> Result<&Vec<u8>, StorageError>;
|
||||
|
||||
/// Deletes the entry identified by `id` from the storage. Implementations should also destroy
|
||||
/// the entries content by overwriting it.
|
||||
async fn erase(&mut self, id: &str) -> Result<(), StorageError>;
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
use crate::{
|
||||
storage::{Storage, StorageError},
|
||||
Session,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, sqlx::FromRow)]
|
||||
struct EntryRow {
|
||||
id: String,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
/// This is the standard implementation of [Storage]
|
||||
pub struct StandardStorage {
|
||||
pool: SqlitePool,
|
||||
entries: HashMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl StandardStorage {
|
||||
/// Creates a new instance of [StandardStorage]. The parameter `db` has to specify a connection
|
||||
/// to a sqlite database in the SQLx format.
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use icrc_client::storage::standard::StandardStorage;
|
||||
///
|
||||
/// let storage = StandardStorage::new("sqlite:/path/to/database.db");
|
||||
/// ```
|
||||
pub async fn new(db: &str) -> Result<Self, sqlx::Error> {
|
||||
let pool = SqlitePool::connect(db).await?;
|
||||
|
||||
sqlx::query(
|
||||
r#"CREATE TABLE IF NOT EXISTS Entries (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
data BLOB NOT NULL
|
||||
);"#,
|
||||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
let mut entries = HashMap::new();
|
||||
let rows: Vec<EntryRow> = sqlx::query_as(r#"SELECT * FROM Entries;"#)
|
||||
.fetch_all(&pool)
|
||||
.await?;
|
||||
for row in rows {
|
||||
entries.insert(row.id, row.data);
|
||||
}
|
||||
|
||||
Ok(Self { pool, entries })
|
||||
}
|
||||
}
|
||||
|
||||
impl Storage for StandardStorage {
|
||||
async fn push(&mut self, id: &str, data: Vec<u8>) -> Result<(), StorageError> {
|
||||
if self.entries.contains_key(id) {
|
||||
return Err(StorageError::IdCollision(id.to_string()));
|
||||
}
|
||||
|
||||
sqlx::query(r#"INSERT INTO Entries VALUES (?, ?);"#)
|
||||
.bind(id)
|
||||
.bind(&data)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| StorageError::Unknown(e.to_string()))?;
|
||||
|
||||
self.entries.insert(id.to_string(), data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn pull(&mut self, id: &str) -> Result<&Vec<u8>, StorageError> {
|
||||
if let Some(data) = self.entries.get(id) {
|
||||
Ok(data)
|
||||
} else {
|
||||
Err(StorageError::IdNotFound(id.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn erase(&mut self, id: &str) -> Result<(), StorageError> {
|
||||
if !self.entries.contains_key(id) {
|
||||
return Err(StorageError::IdNotFound(id.to_string()));
|
||||
}
|
||||
|
||||
sqlx::query(r#"DELETE FROM Entries WHERE id = ?;"#)
|
||||
.bind(id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| StorageError::Unknown(e.to_string()))?;
|
||||
|
||||
self.entries.remove(id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn standard_storage() {
|
||||
let mut storage = StandardStorage::new("sqlite:test.db").await.unwrap();
|
||||
let data = vec![0xAC, 0xAB];
|
||||
storage.push("test", data).await.unwrap();
|
||||
|
||||
let pulled_data = storage.pull("test").await.unwrap();
|
||||
assert!(pulled_data[0] == 0xAC && pulled_data[1] == 0xAB);
|
||||
|
||||
storage.erase("test").await.unwrap();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue