Skip to main content

luct_store/
file.rs

1use luct_core::store::{OrderedStoreRead, SearchableStoreRead, StoreRead, StoreWrite};
2use std::{
3    fs::OpenOptions,
4    io::Write,
5    marker::PhantomData,
6    path::PathBuf,
7    sync::{Arc, Mutex},
8};
9
10use crate::{StringStoreKey, StringStoreValue};
11
12// TODO: Log errors
13
14/// Implementation of [`Store`](luct_core::store::Store) that is backed by a directory.
15///
16/// # Description
17/// [`FilesystemStore`] used a directory named after the store and stores the keys as files.
18/// It requires both [`StringStoreKey`] for keys and [`StringStoreValue`] for values, since
19/// it stores the values as [`Strings`](String) as well.
20///
21/// This implementation is not efficient in any way.
22/// It is fast enough for CLI usage, since the amount of data processed there is relatively small.
23/// Also, storing data as [`Stings`](String) in files makes debugging and understanding what data has
24/// been stored very easy.
25///
26/// Searching through the store is done by scanning through the directory, which is very slow.
27///
28/// # Caution
29/// There is no locking or checking that each path is instanciated only once.
30/// You must be careful not to instanciate two stores at the same location.
31///
32/// Also starting a program that uses the store twice may load to problems.
33/// This is used mainly for simple applications.
34/// You may need a database for more complex applications.
35#[derive(Clone, Debug)]
36pub struct FilesystemStore<K, V> {
37    _kv: PhantomData<(K, V)>,
38    path: PathBuf,
39    access: Arc<Mutex<()>>,
40}
41
42impl<K, V> FilesystemStore<K, V> {
43    /// Create a new [`FilesystemStore`], at the `path`
44    pub fn new(path: PathBuf) -> FilesystemStore<K, V> {
45        std::fs::create_dir_all(&path)
46            .inspect_err(|err| {
47                tracing::error!(
48                    "Failed to create necessary directory {:?} for filesystem store, err: {:?}",
49                    path,
50                    err,
51                )
52            })
53            .expect("Failed to set up filesystem store");
54
55        Self {
56            _kv: PhantomData,
57            path,
58            access: Arc::new(Mutex::new(())),
59        }
60    }
61}
62
63impl<K: StringStoreKey, V: StringStoreValue> FilesystemStore<K, V> {
64    fn get_sorted_keys(&self) -> Option<Vec<K>> {
65        let paths = std::fs::read_dir(&self.path).ok()?;
66        let mut keys = paths
67            .filter_map(|path| match path {
68                Ok(dir_entry) => Some(K::deserialize_key(
69                    &dir_entry.file_name().into_string().unwrap(),
70                ))
71                .flatten(),
72                Err(err) => {
73                    tracing::error!(
74                        "Failed to deserialize a key (get_sorted_keys) err: {:?}",
75                        err
76                    );
77                    None
78                }
79            })
80            .collect::<Vec<_>>();
81        keys.sort();
82
83        Some(keys)
84    }
85}
86
87impl<K: StringStoreKey, V: StringStoreValue> StoreRead<K, V> for FilesystemStore<K, V> {
88    fn get(&self, key: &K) -> Option<V> {
89        let _lock = self.access.lock().unwrap();
90        let data = std::fs::read_to_string(self.path.join(key.serialize_key())).ok()?;
91        let value = V::deserialize_value(&data)?;
92        Some(value)
93    }
94
95    fn len(&self) -> usize {
96        let _lock = self.access.lock().unwrap();
97        match std::fs::read_dir(&self.path) {
98            Ok(paths) => paths.count(),
99            Err(_) => 0,
100        }
101    }
102}
103
104impl<K: StringStoreKey, V: StringStoreValue> StoreWrite<K, V> for FilesystemStore<K, V> {
105    fn insert(&self, key: K, value: V) {
106        let _lock = self.access.lock().unwrap();
107        let store_path = self.path.join(key.serialize_key());
108
109        match OpenOptions::new()
110            .create(true)
111            .truncate(true)
112            .write(true)
113            .open(&store_path)
114        {
115            Ok(mut file) => {
116                file.write_all(value.serialize_value().as_bytes()).unwrap();
117                tracing::debug!("Wrote key to {:?}", store_path);
118            }
119            Err(err) => tracing::error!("Failed to write to path {:?}, err {:?}", store_path, err),
120        };
121    }
122
123    fn delete(&self, key: &K) -> bool {
124        let _lock = self.access.lock().unwrap();
125        std::fs::remove_file(self.path.join(key.serialize_key())).is_ok()
126    }
127}
128
129impl<K: StringStoreKey, V: StringStoreValue> OrderedStoreRead<K, V> for FilesystemStore<K, V> {
130    fn last(&self) -> Option<(K, V)> {
131        let _lock = self.access.lock().unwrap();
132        let keys = self.get_sorted_keys()?;
133
134        // If the last one exists, try to read the value
135        let key = keys.last().cloned()?;
136        let data = std::fs::read_to_string(self.path.join(key.serialize_key())).ok()?;
137        let val = V::deserialize_value(&data)?;
138
139        Some((key, val))
140    }
141}
142
143impl<K: StringStoreKey, V: StringStoreValue> SearchableStoreRead<K, V> for FilesystemStore<K, V> {
144    fn filter(&self, mut pred: impl FnMut(&K, &V) -> bool) -> Vec<(K, V)> {
145        let _lock = self.access.lock().unwrap();
146        let Some(keys) = self.get_sorted_keys() else {
147            return vec![];
148        };
149
150        keys.into_iter()
151            .filter_map(|key| {
152                std::fs::read_to_string(self.path.join(key.serialize_key()))
153                    .ok()
154                    .map(|data| (key, data))
155            })
156            .filter_map(|(key, data)| V::deserialize_value(&data).map(|val| (key, val)))
157            .filter(|(key, val)| pred(key, val))
158            .collect()
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use luct_test::store::{ordered_store_test, searchable_store_test, store_test};
166    use tempdir::TempDir;
167
168    #[test]
169    fn filesystem_store() {
170        let dir = TempDir::new("filesystem_store").unwrap();
171
172        let store = FilesystemStore::<u64, String>::new(dir.path().to_owned());
173        store_test(store);
174    }
175
176    #[test]
177    fn filesystem_ordered_store() {
178        let dir = TempDir::new("filesystem_store").unwrap();
179
180        let store = FilesystemStore::<u64, String>::new(dir.path().to_owned());
181        ordered_store_test(store);
182    }
183
184    #[test]
185    fn filesystem_searchable_store() {
186        let dir = TempDir::new("filesystem_store").unwrap();
187
188        let store = FilesystemStore::<u64, String>::new(dir.path().to_owned());
189        searchable_store_test(store);
190    }
191}