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 = [
|
dependencies = [
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
"sqlx",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
test.db
|
|
@ -6,6 +6,7 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
reqwest = { version = "0.12.2", features = ["json"] }
|
reqwest = { version = "0.12.2", features = ["json"] }
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
|
sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio"] }
|
||||||
thiserror = "1.0.58"
|
thiserror = "1.0.58"
|
||||||
tokio = { version = "1.37.0", features = ["full"] }
|
tokio = { version = "1.37.0", features = ["full"] }
|
||||||
uuid = "1.8.0"
|
uuid = "1.8.0"
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
mod error;
|
pub mod error;
|
||||||
mod session;
|
pub mod session;
|
||||||
|
pub mod storage;
|
||||||
|
|
||||||
pub use session::Session;
|
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