1use crate::error::LaserError;
2use std::collections::HashMap;
3use std::path::{Path, PathBuf};
4use std::sync::Mutex;
5use tokio::fs;
6use tokio::io::AsyncWriteExt;
7
8#[cfg(unix)]
10async fn restrict_to_owner(dir: &Path) -> Result<(), LaserError> {
11 use std::os::unix::fs::PermissionsExt;
12 fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))
13 .await
14 .map_err(|error| LaserError::StateStore(error.to_string()))
15}
16
17#[cfg(not(unix))]
18async fn restrict_to_owner(_dir: &Path) -> Result<(), LaserError> {
19 Ok(())
20}
21
22#[trait_variant::make(StateStore: Send)]
36pub trait LocalStateStore {
37 async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, LaserError>;
38 async fn set(&self, key: &str, value: Vec<u8>) -> Result<(), LaserError>;
39 async fn delete(&self, key: &str) -> Result<(), LaserError>;
40}
41
42#[derive(Default)]
44pub struct InMemoryStore {
45 entries: Mutex<HashMap<String, Vec<u8>>>,
46}
47
48impl InMemoryStore {
49 pub fn new() -> Self {
51 Self::default()
52 }
53
54 fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, Vec<u8>>> {
58 self.entries
59 .lock()
60 .unwrap_or_else(|poisoned| poisoned.into_inner())
61 }
62}
63
64impl StateStore for InMemoryStore {
65 async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, LaserError> {
66 Ok(self.lock().get(key).cloned())
67 }
68
69 async fn set(&self, key: &str, value: Vec<u8>) -> Result<(), LaserError> {
70 self.lock().insert(key.to_owned(), value);
71 Ok(())
72 }
73
74 async fn delete(&self, key: &str) -> Result<(), LaserError> {
75 self.lock().remove(key);
76 Ok(())
77 }
78}
79
80pub struct FileStore {
84 root: PathBuf,
85}
86
87impl FileStore {
88 pub fn new(root: impl Into<PathBuf>) -> Self {
90 Self { root: root.into() }
91 }
92
93 fn path_for(&self, key: &str) -> PathBuf {
94 let mut name = String::with_capacity(key.len() * 2);
95 for byte in key.bytes() {
96 name.push_str(&format!("{byte:02x}"));
97 }
98 self.root.join(name)
99 }
100}
101
102impl StateStore for FileStore {
103 async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, LaserError> {
104 match fs::read(self.path_for(key)).await {
105 Ok(payload) => Ok(Some(payload)),
106 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
107 Err(error) => Err(LaserError::StateStore(error.to_string())),
108 }
109 }
110
111 async fn set(&self, key: &str, value: Vec<u8>) -> Result<(), LaserError> {
112 fs::create_dir_all(&self.root)
113 .await
114 .map_err(|error| LaserError::StateStore(error.to_string()))?;
115 restrict_to_owner(&self.root).await?;
119 let final_path = self.path_for(key);
127 let tmp_path = final_path.with_extension(format!("{}.tmp", ulid::Ulid::generate()));
128 let cleanup_on_err = |error: std::io::Error| {
129 let _ = std::fs::remove_file(&tmp_path);
130 LaserError::StateStore(error.to_string())
131 };
132 let staged = fs::File::create(&tmp_path).await.map_err(cleanup_on_err)?;
136 let mut staged = staged;
137 staged.write_all(&value).await.map_err(cleanup_on_err)?;
138 staged.sync_all().await.map_err(cleanup_on_err)?;
139 drop(staged);
140 fs::rename(&tmp_path, &final_path)
141 .await
142 .map_err(|error| LaserError::StateStore(error.to_string()))?;
143 if let Ok(dir) = std::fs::File::open(&self.root) {
145 let _ = dir.sync_all();
146 }
147 Ok(())
148 }
149
150 async fn delete(&self, key: &str) -> Result<(), LaserError> {
151 match fs::remove_file(self.path_for(key)).await {
152 Ok(()) => Ok(()),
153 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
154 Err(error) => Err(LaserError::StateStore(error.to_string())),
155 }
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 async fn round_trips<S: StateStore>(store: S) {
164 assert_eq!(store.get("missing").await.expect("get"), None);
165 store
166 .set("k", b"v".to_vec())
167 .await
168 .expect("set should succeed");
169 assert_eq!(
170 store.get("k").await.expect("get should succeed"),
171 Some(b"v".to_vec())
172 );
173 store.delete("k").await.expect("delete should succeed");
174 assert_eq!(store.get("k").await.expect("get should succeed"), None);
175 }
176
177 #[tokio::test]
178 async fn given_an_in_memory_store_when_used_then_should_round_trip() {
179 round_trips(InMemoryStore::new()).await;
180 }
181
182 #[tokio::test]
183 async fn given_a_file_store_when_used_then_should_round_trip() {
184 let dir = std::env::temp_dir().join(format!("laser-statestore-{}", std::process::id()));
185 round_trips(FileStore::new(&dir)).await;
186 let _ = fs::remove_dir_all(&dir).await;
187 }
188}