feat(client >> storage): implemented a persistent key-value storage system

This commit is contained in:
antifallobst 2024-03-31 20:44:00 +02:00
parent a3b0263f97
commit 08cf73a613
Signed by: antifallobst
GPG Key ID: 2B4F402172791BAF
6 changed files with 146 additions and 2 deletions

1
Cargo.lock generated
View File

@ -1158,6 +1158,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"reqwest", "reqwest",
"serde", "serde",
"sqlx",
"thiserror", "thiserror",
"tokio", "tokio",
"uuid", "uuid",

1
client/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
test.db

View File

@ -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"

View File

@ -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;

29
client/src/storage/mod.rs Normal file
View File

@ -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>;
}

View File

@ -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();
}
}