millipede_storage_fs/
kvs.rs1use crate::layout::{is_temporary_file, temporary_suffix, validate_key};
2use bytes::Bytes;
3use millipede_core::storage::{
4 KeyInfo, KeyList, KeyValueStore, KvEntry, ListKeysOptions, StorageResult,
5};
6use std::{collections::BTreeMap, path::PathBuf, sync::Arc};
7use tokio::sync::{Mutex, RwLock};
8
9pub struct FsKeyValueStore {
11 name: String,
12 path: PathBuf,
13 operations: Arc<RwLock<()>>,
14 writes: Mutex<()>,
15}
16
17impl FsKeyValueStore {
18 pub(crate) fn open(name: String, path: PathBuf, operations: Arc<RwLock<()>>) -> Self {
19 Self {
20 name,
21 path,
22 operations,
23 writes: Mutex::new(()),
24 }
25 }
26
27 async fn matching_files(&self, key: &str) -> StorageResult<Vec<(String, PathBuf)>> {
28 tokio::fs::create_dir_all(&self.path).await?;
29 let mut entries = tokio::fs::read_dir(&self.path).await?;
30 let mut matches = Vec::new();
31 while let Some(entry) = entries.next_entry().await? {
32 if !entry.file_type().await?.is_file() {
33 continue;
34 }
35 let name = entry.file_name();
36 let Some(name) = name.to_str() else {
37 continue;
38 };
39 if is_temporary_file(name) {
40 continue;
41 }
42 let Some((stored_key, extension)) = name.rsplit_once('.') else {
43 continue;
44 };
45 if stored_key != key || extension.is_empty() {
46 continue;
47 }
48 matches.push((extension.to_owned(), entry.path()));
49 }
50 matches.sort_unstable_by(|left, right| left.1.cmp(&right.1));
51 Ok(matches)
52 }
53}
54
55#[async_trait::async_trait]
56impl KeyValueStore for FsKeyValueStore {
57 async fn get_bytes(&self, key: &str) -> StorageResult<Option<KvEntry>> {
58 validate_key(key)?;
59 let _operation = self.operations.read().await;
60 let _guard = self.writes.lock().await;
61 let Some((extension, path)) = self.matching_files(key).await?.into_iter().next() else {
62 return Ok(None);
63 };
64 Ok(Some(KvEntry {
65 key: key.to_owned(),
66 value: Bytes::from(tokio::fs::read(path).await?),
67 content_type: content_type_for_extension(&extension).to_owned(),
68 }))
69 }
70
71 async fn set_bytes(&self, key: &str, bytes: Bytes, content_type: &str) -> StorageResult<()> {
72 validate_key(key)?;
73 let _operation = self.operations.read().await;
74 let _guard = self.writes.lock().await;
75 tokio::fs::create_dir_all(&self.path).await?;
76 let extension = extension_for_content_type(content_type);
77 let destination = self.path.join(format!("{key}.{extension}"));
78 let temporary = self
79 .path
80 .join(format!("{key}.{extension}.{}", temporary_suffix()));
81 tokio::fs::write(&temporary, &bytes).await?;
82 if let Err(error) = tokio::fs::rename(&temporary, &destination).await {
83 let _ = tokio::fs::remove_file(&temporary).await;
84 return Err(error.into());
85 }
86 for (_, old_path) in self.matching_files(key).await? {
87 if old_path != destination {
88 tokio::fs::remove_file(old_path).await?;
89 }
90 }
91 tracing::trace!(store = %self.name, key, content_type, "stored key-value entry");
92 Ok(())
93 }
94
95 async fn delete(&self, key: &str) -> StorageResult<()> {
96 validate_key(key)?;
97 let _operation = self.operations.read().await;
98 let _guard = self.writes.lock().await;
99 for (_, path) in self.matching_files(key).await? {
100 tokio::fs::remove_file(path).await?;
101 }
102 Ok(())
103 }
104
105 async fn list_keys(&self, opts: ListKeysOptions) -> StorageResult<KeyList> {
110 let _operation = self.operations.read().await;
111 if opts.limit == Some(0) {
112 return Ok(KeyList {
113 keys: Vec::new(),
114 is_truncated: false,
115 next_exclusive_start_key: None,
116 });
117 }
118
119 let _guard = self.writes.lock().await;
120 tokio::fs::create_dir_all(&self.path).await?;
121 let mut entries = tokio::fs::read_dir(&self.path).await?;
122 let mut keys = BTreeMap::new();
123 while let Some(entry) = entries.next_entry().await? {
124 if !entry.file_type().await?.is_file() {
125 continue;
126 }
127 let name = entry.file_name();
128 let Some(name) = name.to_str() else {
129 continue;
130 };
131 if is_temporary_file(name) {
132 continue;
133 }
134 let Some((key, extension)) = name.rsplit_once('.') else {
135 continue;
136 };
137 if key.is_empty() || extension.is_empty() {
138 continue;
139 }
140 let size = entry.metadata().await?.len();
141 keys.entry(key.to_owned()).or_insert(size);
142 }
143
144 let start = opts.exclusive_start_key.as_deref();
145 let mut filtered = keys
146 .into_iter()
147 .filter(|(key, _)| start.is_none_or(|start| key.as_str() > start));
148 let limit = opts.limit.unwrap_or(usize::MAX);
149 let selected: Vec<_> = filtered.by_ref().take(limit).collect();
150 let is_truncated = filtered.next().is_some();
151 let next_exclusive_start_key = is_truncated
152 .then(|| selected.last().map(|(key, _)| key.clone()))
153 .flatten();
154 Ok(KeyList {
155 keys: selected
156 .into_iter()
157 .map(|(key, size)| KeyInfo { key, size })
158 .collect(),
159 is_truncated,
160 next_exclusive_start_key,
161 })
162 }
163}
164
165fn extension_for_content_type(content_type: &str) -> &'static str {
166 match content_type
167 .split(';')
168 .next()
169 .unwrap_or_default()
170 .trim()
171 .to_ascii_lowercase()
172 .as_str()
173 {
174 "application/json" => "json",
175 "text/plain" => "txt",
176 "text/html" => "html",
177 "application/xml" | "text/xml" => "xml",
178 "image/png" => "png",
179 "image/jpeg" => "jpeg",
180 "application/octet-stream" => "bin",
181 _ => "bin",
182 }
183}
184
185fn content_type_for_extension(extension: &str) -> &'static str {
186 match extension.to_ascii_lowercase().as_str() {
187 "json" => "application/json",
188 "txt" => "text/plain",
189 "html" => "text/html",
190 "xml" => "application/xml",
191 "png" => "image/png",
192 "jpeg" => "image/jpeg",
193 "bin" => "application/octet-stream",
194 _ => "application/octet-stream",
195 }
196}