1use std::{
9 fs, io,
10 path::{Path, PathBuf},
11 sync::Arc,
12};
13use wcore::Memory;
14
15#[derive(Debug, Clone)]
19pub struct FsMemory {
20 base: Arc<PathBuf>,
21}
22
23impl FsMemory {
24 pub fn new(base: impl Into<PathBuf>) -> io::Result<Self> {
26 let base = base.into();
27 fs::create_dir_all(&base)?;
28 Ok(Self {
29 base: Arc::new(base),
30 })
31 }
32
33 fn key_to_path(&self, key: &str) -> PathBuf {
34 let mut path = (*self.base).clone();
35 let parts: Vec<&str> = key.split('.').collect();
36 for part in &parts {
37 path.push(part);
38 }
39 path.set_extension("md");
40 path
41 }
42
43 fn path_to_key(base: &Path, path: &Path) -> Option<String> {
44 let rel = path.strip_prefix(base).ok()?;
45 let without_ext = rel.with_extension("");
46 let key = without_ext
47 .components()
48 .map(|c| c.as_os_str().to_string_lossy())
49 .collect::<Vec<_>>()
50 .join(".");
51 if key.is_empty() { None } else { Some(key) }
52 }
53
54 fn collect_entries(dir: &Path, base: &Path, out: &mut Vec<(String, String)>) {
55 let Ok(read) = fs::read_dir(dir) else {
56 return;
57 };
58 for entry in read.flatten() {
59 let path = entry.path();
60 if path.is_dir() {
61 Self::collect_entries(&path, base, out);
62 } else if path.extension().and_then(|e| e.to_str()) == Some("md")
63 && let (Some(key), Ok(value)) =
64 (Self::path_to_key(base, &path), fs::read_to_string(&path))
65 {
66 out.push((key, value));
67 }
68 }
69 }
70}
71
72impl Memory for FsMemory {
73 fn get(&self, key: &str) -> Option<String> {
74 let path = self.key_to_path(key);
75 fs::read_to_string(path).ok()
76 }
77
78 fn entries(&self) -> Vec<(String, String)> {
79 let mut out = Vec::new();
80 Self::collect_entries(&self.base, &self.base, &mut out);
81 out
82 }
83
84 fn set(&self, key: impl Into<String>, value: impl Into<String>) -> Option<String> {
85 let key = key.into();
86 let value = value.into();
87 let path = self.key_to_path(&key);
88
89 let old = fs::read_to_string(&path).ok();
91
92 if let Some(parent) = path.parent() {
94 fs::create_dir_all(parent).ok()?;
95 }
96
97 let tmp = path.with_extension("md.tmp");
99 fs::write(&tmp, &value).ok()?;
100 fs::rename(&tmp, &path).ok()?;
101
102 old
103 }
104
105 fn remove(&self, key: &str) -> Option<String> {
106 let path = self.key_to_path(key);
107 let old = fs::read_to_string(&path).ok()?;
108 fs::remove_file(&path).ok()?;
109
110 let mut current = path.parent();
112 while let Some(dir) = current {
113 if dir == *self.base {
114 break;
115 }
116 if fs::read_dir(dir)
117 .map(|mut d| d.next().is_none())
118 .unwrap_or(false)
119 {
120 fs::remove_dir(dir).ok();
121 } else {
122 break;
123 }
124 current = dir.parent();
125 }
126
127 Some(old)
128 }
129}