Skip to main content

luct_store/
file.rs

1use luct_core::store::{OrderedStoreRead, SearchableStoreRead, StoreBase, 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, V> StoreBase for FilesystemStore<K, V> {
88    type Key = K;
89    type Value = V;
90}
91
92impl<K: StringStoreKey, V: StringStoreValue> StoreRead for FilesystemStore<K, V> {
93    fn get(&self, key: &K) -> Option<V> {
94        let _lock = self.access.lock().unwrap();
95        let data = std::fs::read_to_string(self.path.join(key.serialize_key())).ok()?;
96        let value = V::deserialize_value(&data)?;
97        Some(value)
98    }
99
100    fn len(&self) -> usize {
101        let _lock = self.access.lock().unwrap();
102        match std::fs::read_dir(&self.path) {
103            Ok(paths) => paths.count(),
104            Err(_) => 0,
105        }
106    }
107}
108
109impl<K, V> StoreWrite for FilesystemStore<K, V>
110where
111    K: StringStoreKey,
112    V: StringStoreValue,
113{
114    fn insert(&self, key: K, value: V) {
115        let _lock = self.access.lock().unwrap();
116        let store_path = self.path.join(key.serialize_key());
117
118        match OpenOptions::new()
119            .create(true)
120            .truncate(true)
121            .write(true)
122            .open(&store_path)
123        {
124            Ok(mut file) => {
125                file.write_all(value.serialize_value().as_bytes()).unwrap();
126                tracing::debug!("Wrote key to {:?}", store_path);
127            }
128            Err(err) => tracing::error!("Failed to write to path {:?}, err {:?}", store_path, err),
129        };
130    }
131
132    fn delete(&self, key: &K) -> bool {
133        let _lock = self.access.lock().unwrap();
134        std::fs::remove_file(self.path.join(key.serialize_key())).is_ok()
135    }
136}
137
138impl<K, V> OrderedStoreRead for FilesystemStore<K, V>
139where
140    K: StringStoreKey,
141    V: StringStoreValue,
142{
143    fn last(&self) -> Option<(K, V)> {
144        let _lock = self.access.lock().unwrap();
145        let keys = self.get_sorted_keys()?;
146
147        // If the last one exists, try to read the value
148        let key = keys.last().cloned()?;
149        let data = std::fs::read_to_string(self.path.join(key.serialize_key())).ok()?;
150        let val = V::deserialize_value(&data)?;
151
152        Some((key, val))
153    }
154}
155
156impl<K, V> SearchableStoreRead for FilesystemStore<K, V>
157where
158    K: StringStoreKey,
159    V: StringStoreValue,
160{
161    fn filter(&self, mut pred: impl FnMut(&K, &V) -> bool) -> Vec<(K, V)> {
162        let _lock = self.access.lock().unwrap();
163        let Some(keys) = self.get_sorted_keys() else {
164            return vec![];
165        };
166
167        keys.into_iter()
168            .filter_map(|key| {
169                std::fs::read_to_string(self.path.join(key.serialize_key()))
170                    .ok()
171                    .map(|data| (key, data))
172            })
173            .filter_map(|(key, data)| V::deserialize_value(&data).map(|val| (key, val)))
174            .filter(|(key, val)| pred(key, val))
175            .collect()
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use luct_test::store::{ordered_store_test, searchable_store_test, store_test};
183    use tempfile::TempDir;
184
185    #[test]
186    fn filesystem_store() {
187        let dir = TempDir::new().unwrap();
188
189        let store = FilesystemStore::<u64, String>::new(dir.path().to_owned());
190        store_test(store);
191    }
192
193    #[test]
194    fn filesystem_ordered_store() {
195        let dir = TempDir::new().unwrap();
196
197        let store = FilesystemStore::<u64, String>::new(dir.path().to_owned());
198        ordered_store_test(store);
199    }
200
201    #[test]
202    fn filesystem_searchable_store() {
203        let dir = TempDir::new().unwrap();
204
205        let store = FilesystemStore::<u64, String>::new(dir.path().to_owned());
206        searchable_store_test(store);
207    }
208}