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