Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fs-storage: Port BaseStorage abstraction from Kotlin #23

Merged
merged 11 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions fs-storage/examples/cli.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use anyhow::{Context, Result};
use fs_storage::base_storage::BaseStorage;
use fs_storage::file_storage::FileStorage;
use serde_json::Value;
use std::collections::BTreeMap;
Expand Down Expand Up @@ -45,7 +46,7 @@ fn read_command(args: &[String], path: &str) -> Result<()> {

let mut fs = FileStorage::new("cli".to_string(), Path::new(path));
let map: BTreeMap<String, String> =
fs.read_file().context("Failed to read file")?;
fs.read_fs().context("Failed to read file")?;

if keys.is_empty() {
for (key, value) in map {
Expand Down Expand Up @@ -76,7 +77,7 @@ fn write_command(args: &[String], path: &str) -> Result<()> {
.extension()
.map_or(false, |ext| ext == "json");

let mut kv_pairs = BTreeMap::new();
let mut fs = FileStorage::new("cli".to_string(), Path::new(path));
if content_json {
let content =
fs::read_to_string(content).context("Failed to read JSON file")?;
Expand All @@ -85,7 +86,7 @@ fn write_command(args: &[String], path: &str) -> Result<()> {
if let Value::Object(object) = json {
for (key, value) in object {
if let Value::String(value_str) = value {
kv_pairs.insert(key, value_str);
fs.set(key, value_str);
} else {
println!(
"Warning: Skipping non-string value for key '{}'",
Expand All @@ -102,14 +103,12 @@ fn write_command(args: &[String], path: &str) -> Result<()> {
for pair in pairs {
let kv: Vec<&str> = pair.split(':').collect();
if kv.len() == 2 {
kv_pairs.insert(kv[0].to_string(), kv[1].to_string());
fs.set(kv[0].to_string(), kv[1].to_string());
}
}
}

let mut fs = FileStorage::new("cli".to_string(), Path::new(path));
fs.write_file(&kv_pairs)
.context("Failed to write file")?;
fs.write_fs().context("Failed to write file")?;

Ok(())
}
41 changes: 41 additions & 0 deletions fs-storage/src/base_storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use std::collections::BTreeMap;
use std::fmt::Debug;
use std::hash::Hash;
use std::str::FromStr;

use data_error::Result;

pub trait BaseStorage<K, V>
where
K: FromStr + Hash + Eq + Ord + Debug + Clone,
V: Debug + Clone,
{
fn get(&self, id: &K) -> Option<&V>;
tareknaser marked this conversation as resolved.
Show resolved Hide resolved
fn set(&mut self, id: K, value: V);
tareknaser marked this conversation as resolved.
Show resolved Hide resolved
fn remove(&mut self, id: &K) -> Result<()>;

/// Remove file at stored path
fn erase(&self) -> Result<()>;

/// Get immutable BTreeMap
///
/// This can be used to get most immutable BtreeMap related functions for free.
fn as_ref(&self) -> &BTreeMap<K, V>;
Pushkarm029 marked this conversation as resolved.
Show resolved Hide resolved

/// Check if storage is updated
///
/// This check can be used before reading the file.
fn is_storage_updated(&self) -> Result<bool>;

/// Read data from disk
///
/// Data is read as key value pairs separated by a symbol and stored
/// in a [BTreeMap] with a generic key K and V value. A handler
/// is called on the data after reading it.
fn read_fs(&mut self) -> Result<BTreeMap<K, V>>;

/// Write data to file
///
/// Data is a key-value mapping between [ResourceId] and a generic Value
fn write_fs(&mut self) -> Result<()>;
}
188 changes: 100 additions & 88 deletions fs-storage/src/file_storage.rs
Pushkarm029 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::fmt::Debug;
use std::fs::{self, File};
use std::hash::Hash;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::str::FromStr;
use std::time::SystemTime;
Expand All @@ -8,51 +9,111 @@ use std::{
path::{Path, PathBuf},
};

use crate::base_storage::BaseStorage;
use data_error::{ArklibError, Result};

const STORAGE_VERSION: i32 = 2;
const STORAGE_VERSION_PREFIX: &str = "version ";

pub struct FileStorage {
pub struct FileStorage<K, V> {
label: String,
path: PathBuf,
timestamp: SystemTime,
data: BTreeMap<K, V>,
}

impl FileStorage {
impl<K, V> FileStorage<K, V>
where
K: serde::Serialize,
twitu marked this conversation as resolved.
Show resolved Hide resolved
V: serde::Serialize,
{
/// Create a new file storage with a diagnostic label and file path
pub fn new(label: String, path: &Path) -> Self {
Self {
label,
path: PathBuf::from(path),
timestamp: SystemTime::now(),
data: BTreeMap::new(),
Pushkarm029 marked this conversation as resolved.
Show resolved Hide resolved
}
}

/// Check if underlying file has been updated
///
/// This check can be used before reading the file.
pub fn is_file_updated(&self) -> Result<bool> {
/// Verify the version stored in the file header
fn verify_version(&self, header: &str) -> Result<()> {
if !header.starts_with(STORAGE_VERSION_PREFIX) {
return Err(ArklibError::Storage(
self.label.clone(),
"Unknown storage version prefix".to_owned(),
));
}

let version = header[STORAGE_VERSION_PREFIX.len()..]
.parse::<i32>()
.map_err(|_err| {
ArklibError::Storage(
self.label.clone(),
"Failed to parse storage version".to_owned(),
)
})?;

if version != STORAGE_VERSION {
return Err(ArklibError::Storage(
self.label.clone(),
format!(
"Storage version mismatch: expected {}, found {}",
STORAGE_VERSION, version
),
));
}

Ok(())
}
}

impl<K, V> BaseStorage<K, V> for FileStorage<K, V>
where
K: FromStr
+ Hash
+ Eq
+ Ord
+ Debug
+ Clone
+ serde::Serialize
+ serde::de::DeserializeOwned,
V: Debug + Clone + serde::Serialize + serde::de::DeserializeOwned,
{
fn get(&self, id: &K) -> Option<&V> {
self.data.get(id)
}

fn set(&mut self, id: K, value: V) {
self.data.insert(id, value);
self.timestamp = std::time::SystemTime::now();
}

fn remove(&mut self, id: &K) -> Result<()> {
Pushkarm029 marked this conversation as resolved.
Show resolved Hide resolved
self.data.remove(id).ok_or_else(|| {
ArklibError::Storage(self.label.clone(), "Key not found".to_owned())
})?;
self.timestamp = std::time::SystemTime::now();
Ok(())
}

fn erase(&self) -> Result<()> {
fs::remove_file(&self.path).map_err(|err| {
ArklibError::Storage(self.label.clone(), err.to_string())
})
}

fn as_ref(&self) -> &BTreeMap<K, V> {
&self.data
}

fn is_storage_updated(&self) -> Result<bool> {
Pushkarm029 marked this conversation as resolved.
Show resolved Hide resolved
let file_timestamp = fs::metadata(&self.path)?.modified()?;
Ok(self.timestamp < file_timestamp)
}

/// Read data from disk
///
/// Data is read as key value pairs separated by a symbol and stored
/// in a [BTreeMap] with a generic key K and V value. A handler
/// is called on the data after reading it.
pub fn read_file<K, V>(&mut self) -> Result<BTreeMap<K, V>>
where
K: serde::de::DeserializeOwned
+ FromStr
+ std::hash::Hash
+ std::cmp::Eq
+ Debug
+ std::cmp::Ord,
V: serde::de::DeserializeOwned + Debug,
ArklibError: From<<K as FromStr>::Err>,
{
fn read_fs(&mut self) -> Result<BTreeMap<K, V>> {
let file = fs::File::open(&self.path)?;
let reader = BufReader::new(file);
let mut lines = reader.lines();
Expand All @@ -70,10 +131,9 @@ impl FileStorage {
}
data.push_str(&line);
}
let value_by_id = serde_json::from_str(&data)?;

let data: BTreeMap<K, V> = serde_json::from_str(&data)?;
self.timestamp = new_timestamp;
Ok(value_by_id)
Ok(data)
}
None => Err(ArklibError::Storage(
self.label.clone(),
Expand All @@ -82,17 +142,7 @@ impl FileStorage {
}
}

/// Write data to file
///
/// Data is a key-value mapping between [ResourceId] and a generic Value
pub fn write_file<K, V>(
&mut self,
value_by_id: &BTreeMap<K, V>,
) -> Result<()>
where
K: serde::Serialize,
V: serde::Serialize,
{
fn write_fs(&mut self) -> Result<()> {
let parent_dir = self.path.parent().ok_or_else(|| {
ArklibError::Storage(
self.label.clone(),
Expand All @@ -108,8 +158,9 @@ impl FileStorage {
.as_bytes(),
)?;

let data = serde_json::to_string(value_by_id)?;
writer.write_all(data.as_bytes())?;
let value_map = self.data.clone();
let value_data = serde_json::to_string(&value_map)?;
writer.write_all(value_data.as_bytes())?;

let new_timestamp = fs::metadata(&self.path)?.modified()?;
if new_timestamp == self.timestamp {
Expand All @@ -120,56 +171,18 @@ impl FileStorage {
log::info!(
"{} {} entries have been written",
self.label,
value_by_id.len()
value_map.len()
);
Ok(())
}

/// Remove file at stored path
pub fn erase(&self) -> Result<()> {
fs::remove_file(&self.path).map_err(|err| {
ArklibError::Storage(self.label.clone(), err.to_string())
})
}

/// Verify the version stored in the file header
fn verify_version(&self, header: &str) -> Result<()> {
if !header.starts_with(STORAGE_VERSION_PREFIX) {
return Err(ArklibError::Storage(
self.label.clone(),
"Unknown storage version prefix".to_owned(),
));
}

let version = header[STORAGE_VERSION_PREFIX.len()..]
.parse::<i32>()
.map_err(|_err| {
ArklibError::Storage(
self.label.clone(),
"Failed to parse storage version".to_owned(),
)
})?;

if version != STORAGE_VERSION {
return Err(ArklibError::Storage(
self.label.clone(),
format!(
"Storage version mismatch: expected {}, found {}",
STORAGE_VERSION, version
),
));
}

Ok(())
}
}

#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use tempdir::TempDir;

use crate::file_storage::FileStorage;
use crate::{base_storage::BaseStorage, file_storage::FileStorage};

#[test]
fn test_file_storage_write_read() {
Expand All @@ -180,19 +193,19 @@ mod tests {
let mut file_storage =
FileStorage::new("TestStorage".to_string(), &storage_path);

let mut data_to_write = BTreeMap::new();
data_to_write.insert("key1".to_string(), "value1".to_string());
data_to_write.insert("key2".to_string(), "value2".to_string());
file_storage.set("key1".to_string(), "value1".to_string());
file_storage.set("key1".to_string(), "value2".to_string());

file_storage
.write_file(&data_to_write)
.write_fs()
.expect("Failed to write data to disk");

let data_read: BTreeMap<_, _> = file_storage
.read_file()
.read_fs()
.expect("Failed to read data from disk");

assert_eq!(data_read, data_to_write);
assert_eq!(data_read.len(), 1);
assert_eq!(data_read.get("key1").map(|v| v.as_str()), Some("value2"))
}

#[test]
Expand All @@ -204,12 +217,11 @@ mod tests {
let mut file_storage =
FileStorage::new("TestStorage".to_string(), &storage_path);

let mut data_to_write = BTreeMap::new();
data_to_write.insert("key1".to_string(), "value1".to_string());
data_to_write.insert("key2".to_string(), "value2".to_string());
file_storage.set("key1".to_string(), "value1".to_string());
file_storage.set("key1".to_string(), "value2".to_string());

file_storage
.write_file(&data_to_write)
.write_fs()
.expect("Failed to write data to disk");

assert_eq!(storage_path.exists(), true);
Expand Down
1 change: 1 addition & 0 deletions fs-storage/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod base_storage;
pub mod file_storage;
pub const ARK_FOLDER: &str = ".ark";

Expand Down
Loading