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#[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 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 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}