1use std::cell::RefCell;
6use std::collections::BTreeMap;
7use std::fmt;
8
9pub trait Storage {
22 fn read(&self, name: &str) -> Result<Option<Vec<u8>>, String>;
24 fn write(&self, name: &str, bytes: &[u8]) -> Result<(), String>;
26 fn remove(&self, name: &str) -> Result<(), String>;
28}
29
30#[derive(Default)]
32#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
33pub struct MemoryStorage(RefCell<BTreeMap<String, Vec<u8>>>);
34
35impl fmt::Debug for MemoryStorage {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 f.debug_struct("MemoryStorage").field("names", &self.0.borrow().keys().collect::<Vec<_>>()).finish()
38 }
39}
40
41impl Storage for MemoryStorage {
42 fn read(&self, name: &str) -> Result<Option<Vec<u8>>, String> {
43 Ok(self.0.borrow().get(name).cloned())
44 }
45
46 fn write(&self, name: &str, bytes: &[u8]) -> Result<(), String> {
47 self.0.borrow_mut().insert(name.to_owned(), bytes.to_vec());
48 Ok(())
49 }
50
51 fn remove(&self, name: &str) -> Result<(), String> {
52 self.0.borrow_mut().remove(name);
53 Ok(())
54 }
55}
56
57impl<S: Storage + ?Sized> Storage for std::rc::Rc<S> {
58 fn read(&self, name: &str) -> Result<Option<Vec<u8>>, String> {
59 (**self).read(name)
60 }
61
62 fn write(&self, name: &str, bytes: &[u8]) -> Result<(), String> {
63 (**self).write(name, bytes)
64 }
65
66 fn remove(&self, name: &str) -> Result<(), String> {
67 (**self).remove(name)
68 }
69}
70
71#[cfg(not(target_arch = "wasm32"))]
74#[derive(Clone, Debug)]
75#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
76pub struct FileStorage {
77 folder: std::path::PathBuf,
78}
79
80#[cfg(not(target_arch = "wasm32"))]
81#[cfg_attr(feature = "live", ravenrs_live::expose)]
82impl FileStorage {
83 pub fn new(folder: impl Into<std::path::PathBuf>) -> Self {
85 Self { folder: folder.into() }
86 }
87
88 fn path(&self, name: &str) -> Result<std::path::PathBuf, String> {
89 let plain = !name.is_empty()
90 && name.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
91 && !name.starts_with('.');
92 if plain {
93 Ok(self.folder.join(name))
94 } else {
95 Err(format!("{name:?} is not a plain name: use letters, digits, '-', '_' and '.'"))
96 }
97 }
98}
99
100#[cfg(not(target_arch = "wasm32"))]
101impl Storage for FileStorage {
102 fn read(&self, name: &str) -> Result<Option<Vec<u8>>, String> {
103 let path = self.path(name)?;
104 match std::fs::read(&path) {
105 Ok(bytes) => Ok(Some(bytes)),
106 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
107 Err(e) => Err(format!("{}: {e}", path.display())),
108 }
109 }
110
111 fn write(&self, name: &str, bytes: &[u8]) -> Result<(), String> {
112 let path = self.path(name)?;
113 let failed = |e: std::io::Error| format!("{}: {e}", path.display());
114 std::fs::create_dir_all(&self.folder).map_err(failed)?;
115 let draft = path.with_extension("draft");
116 std::fs::write(&draft, bytes).map_err(failed)?;
117 std::fs::rename(&draft, &path).map_err(failed)
118 }
119
120 fn remove(&self, name: &str) -> Result<(), String> {
121 let path = self.path(name)?;
122 match std::fs::remove_file(&path) {
123 Err(e) if e.kind() != std::io::ErrorKind::NotFound => Err(format!("{}: {e}", path.display())),
124 _ => Ok(()),
125 }
126 }
127}
128
129#[cfg(all(test, not(target_arch = "wasm32")))]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn files_keep_what_was_written_and_refuse_paths() {
135 let folder = std::env::temp_dir().join(format!("ravenrs-storage-{}", std::process::id()));
136 let storage = FileStorage::new(&folder);
137 assert_eq!(storage.read("records"), Ok(None));
138 assert_eq!(storage.write("records", b"one"), Ok(()));
139 assert_eq!(storage.write("records", b"two"), Ok(()));
140 assert_eq!(storage.read("records"), Ok(Some(b"two".to_vec())));
141 assert!(storage.write("../outside", b"x").is_err());
142 assert!(storage.read(".hidden").is_err());
143 assert_eq!(storage.remove("records"), Ok(()));
144 assert_eq!(storage.remove("records"), Ok(()), "nothing to forget is fine");
145 assert_eq!(storage.read("records"), Ok(None));
146 let _ = std::fs::remove_dir_all(folder);
147 }
148}