luct-store 0.2.0

Collection of storage backends for the luct project
Documentation
use luct_core::store::{OrderedStoreRead, SearchableStoreRead, StoreBase, StoreRead, StoreWrite};
use std::{
    fs::OpenOptions,
    io::Write,
    marker::PhantomData,
    path::PathBuf,
    sync::{Arc, Mutex},
};

use crate::{StringStoreKey, StringStoreValue};

// TODO: Log errors

/// Implementation of [`Store`](luct_core::store::Store) that is backed by a directory.
///
/// # Description
/// [`FilesystemStore`] used a directory named after the store and stores the keys as files.
/// It requires both [`StringStoreKey`] for keys and [`StringStoreValue`] for values, since
/// it stores the values as [`Strings`](String) as well.
///
/// This implementation is not efficient in any way.
/// It is fast enough for CLI usage, since the amount of data processed there is relatively small.
/// Also, storing data as [`Stings`](String) in files makes debugging and understanding what data has
/// been stored very easy.
///
/// Searching through the store is done by scanning through the directory, which is very slow.
///
/// # Caution
/// There is no locking or checking that each path is instanciated only once.
/// You must be careful not to instanciate two stores at the same location.
///
/// Also starting a program that uses the store twice may load to problems.
/// This is used mainly for simple applications.
/// You may need a database for more complex applications.
#[derive(Clone, Debug)]
pub struct FilesystemStore<K, V> {
    _kv: PhantomData<(K, V)>,
    path: PathBuf,
    access: Arc<Mutex<()>>,
}

impl<K, V> FilesystemStore<K, V> {
    /// Create a new [`FilesystemStore`], at the `path`
    pub fn new(path: PathBuf) -> FilesystemStore<K, V> {
        std::fs::create_dir_all(&path)
            .inspect_err(|err| {
                tracing::error!(
                    "Failed to create necessary directory {:?} for filesystem store, err: {:?}",
                    path,
                    err,
                )
            })
            .expect("Failed to set up filesystem store");

        Self {
            _kv: PhantomData,
            path,
            access: Arc::new(Mutex::new(())),
        }
    }
}

impl<K: StringStoreKey, V: StringStoreValue> FilesystemStore<K, V> {
    fn get_sorted_keys(&self) -> Option<Vec<K>> {
        let paths = std::fs::read_dir(&self.path).ok()?;
        let mut keys = paths
            .filter_map(|path| match path {
                Ok(dir_entry) => Some(K::deserialize_key(
                    &dir_entry.file_name().into_string().unwrap(),
                ))
                .flatten(),
                Err(err) => {
                    tracing::error!(
                        "Failed to deserialize a key (get_sorted_keys) err: {:?}",
                        err
                    );
                    None
                }
            })
            .collect::<Vec<_>>();
        keys.sort();

        Some(keys)
    }
}

impl<K, V> StoreBase for FilesystemStore<K, V> {
    type Key = K;
    type Value = V;
}

impl<K: StringStoreKey, V: StringStoreValue> StoreRead for FilesystemStore<K, V> {
    fn get(&self, key: &K) -> Option<V> {
        let _lock = self.access.lock().unwrap();
        let data = std::fs::read_to_string(self.path.join(key.serialize_key())).ok()?;
        let value = V::deserialize_value(&data)?;
        Some(value)
    }

    fn len(&self) -> usize {
        let _lock = self.access.lock().unwrap();
        match std::fs::read_dir(&self.path) {
            Ok(paths) => paths.count(),
            Err(_) => 0,
        }
    }
}

impl<K, V> StoreWrite for FilesystemStore<K, V>
where
    K: StringStoreKey,
    V: StringStoreValue,
{
    fn insert(&self, key: K, value: V) {
        let _lock = self.access.lock().unwrap();
        let store_path = self.path.join(key.serialize_key());

        match OpenOptions::new()
            .create(true)
            .truncate(true)
            .write(true)
            .open(&store_path)
        {
            Ok(mut file) => {
                file.write_all(value.serialize_value().as_bytes()).unwrap();
                tracing::debug!("Wrote key to {:?}", store_path);
            }
            Err(err) => tracing::error!("Failed to write to path {:?}, err {:?}", store_path, err),
        };
    }

    fn delete(&self, key: &K) -> bool {
        let _lock = self.access.lock().unwrap();
        std::fs::remove_file(self.path.join(key.serialize_key())).is_ok()
    }
}

impl<K, V> OrderedStoreRead for FilesystemStore<K, V>
where
    K: StringStoreKey,
    V: StringStoreValue,
{
    fn last(&self) -> Option<(K, V)> {
        let _lock = self.access.lock().unwrap();
        let keys = self.get_sorted_keys()?;

        // If the last one exists, try to read the value
        let key = keys.last().cloned()?;
        let data = std::fs::read_to_string(self.path.join(key.serialize_key())).ok()?;
        let val = V::deserialize_value(&data)?;

        Some((key, val))
    }
}

impl<K, V> SearchableStoreRead for FilesystemStore<K, V>
where
    K: StringStoreKey,
    V: StringStoreValue,
{
    fn filter(&self, mut pred: impl FnMut(&K, &V) -> bool) -> Vec<(K, V)> {
        let _lock = self.access.lock().unwrap();
        let Some(keys) = self.get_sorted_keys() else {
            return vec![];
        };

        keys.into_iter()
            .filter_map(|key| {
                std::fs::read_to_string(self.path.join(key.serialize_key()))
                    .ok()
                    .map(|data| (key, data))
            })
            .filter_map(|(key, data)| V::deserialize_value(&data).map(|val| (key, val)))
            .filter(|(key, val)| pred(key, val))
            .collect()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use luct_test::store::{ordered_store_test, searchable_store_test, store_test};
    use tempfile::TempDir;

    #[test]
    fn filesystem_store() {
        let dir = TempDir::new().unwrap();

        let store = FilesystemStore::<u64, String>::new(dir.path().to_owned());
        store_test(store);
    }

    #[test]
    fn filesystem_ordered_store() {
        let dir = TempDir::new().unwrap();

        let store = FilesystemStore::<u64, String>::new(dir.path().to_owned());
        ordered_store_test(store);
    }

    #[test]
    fn filesystem_searchable_store() {
        let dir = TempDir::new().unwrap();

        let store = FilesystemStore::<u64, String>::new(dir.path().to_owned());
        searchable_store_test(store);
    }
}