1use std::fs;
2use std::path::PathBuf;
3use std::time::SystemTime;
4
5#[derive(Clone)]
6pub struct Document {
7 pub name: String,
8 pub path: PathBuf,
9 pub content: String,
10 pub modified_time: Option<SystemTime>,
11}
12
13pub struct DocumentStore {
14 base_path: PathBuf,
15}
16
17impl DocumentStore {
18 pub fn new(base_path: PathBuf) -> Self {
19 DocumentStore { base_path }
20 }
21
22 pub fn load(&self, name: &str) -> Result<Document, String> {
25 let mut path = self.base_path.join(name);
26
27 if path.extension().is_none() {
29 path.set_extension("md");
30 }
31
32 let (content, modified_time) = if path.exists() {
34 let content = fs::read_to_string(&path)
35 .map_err(|e| format!("Failed to read '{}': {}", name, e))?;
36
37 let mtime = fs::metadata(&path).ok().and_then(|m| m.modified().ok());
39
40 (content, mtime)
41 } else {
42 (String::new(), None)
43 };
44
45 Ok(Document {
46 name: name.to_string(),
47 path,
48 content,
49 modified_time,
50 })
51 }
52
53 pub fn list_all_documents(&self) -> Result<Vec<String>, String> {
56 let mut docs = Vec::new();
57 Self::walk_directory(&self.base_path, "", &mut docs)?;
58 Ok(docs)
59 }
60
61 fn walk_directory(dir: &PathBuf, prefix: &str, docs: &mut Vec<String>) -> Result<(), String> {
63 let entries = fs::read_dir(dir)
64 .map_err(|e| format!("Failed to read directory '{}': {}", dir.display(), e))?;
65
66 for entry in entries.flatten() {
67 let path = entry.path();
68
69 if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("md") {
70 if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
71 let full_name = if prefix.is_empty() {
72 name.to_string()
73 } else {
74 format!("{}/{}", prefix, name)
75 };
76 docs.push(full_name);
77 }
78 } else if path.is_dir() {
79 if let Some(dir_name) = path.file_name().and_then(|s| s.to_str()) {
81 let new_prefix = if prefix.is_empty() {
82 dir_name.to_string()
83 } else {
84 format!("{}/{}", prefix, dir_name)
85 };
86 Self::walk_directory(&path, &new_prefix, docs)?;
87 }
88 }
89 }
90
91 Ok(())
92 }
93
94 pub fn save(&self, doc: &Document) -> Result<(), String> {
97 if let Some(parent) = doc.path.parent() {
99 fs::create_dir_all(parent)
100 .map_err(|e| format!("Failed to create directories for '{}': {}", doc.name, e))?;
101 }
102
103 fs::write(&doc.path, &doc.content)
104 .map_err(|e| format!("Failed to save '{}': {}", doc.name, e))
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use std::env;
112
113 #[test]
114 fn test_load_existing_file() {
115 let store = DocumentStore::new("example-wiki".into());
116 let doc = store.load("frontpage").unwrap();
117 assert!(!doc.content.is_empty());
118 assert_eq!(doc.name, "frontpage");
119 }
120
121 #[test]
122 fn test_load_non_existent_file() {
123 let temp_dir = env::temp_dir().join("piki-test-load");
124 let _ = fs::remove_dir_all(&temp_dir);
125 fs::create_dir_all(&temp_dir).unwrap();
126
127 let store = DocumentStore::new(temp_dir.clone());
128 let doc = store.load("non-existent").unwrap();
129
130 assert_eq!(doc.content, "");
131 assert_eq!(doc.name, "non-existent");
132 assert_eq!(doc.path, temp_dir.join("non-existent.md"));
133
134 fs::remove_dir_all(&temp_dir).ok();
136 }
137
138 #[test]
139 fn test_load_nested_path() {
140 let temp_dir = env::temp_dir().join("piki-test-nested");
141 let _ = fs::remove_dir_all(&temp_dir);
142 fs::create_dir_all(&temp_dir).unwrap();
143
144 let store = DocumentStore::new(temp_dir.clone());
145 let doc = store.load("project-a/standup").unwrap();
146
147 assert_eq!(doc.content, "");
148 assert_eq!(doc.name, "project-a/standup");
149 assert_eq!(doc.path, temp_dir.join("project-a/standup.md"));
150
151 fs::remove_dir_all(&temp_dir).ok();
153 }
154
155 #[test]
156 fn test_save_creates_parent_directories() {
157 let temp_dir = env::temp_dir().join("piki-test-save");
158 let _ = fs::remove_dir_all(&temp_dir);
159 fs::create_dir_all(&temp_dir).unwrap();
160
161 let store = DocumentStore::new(temp_dir.clone());
162 let mut doc = store.load("nested/dir/page").unwrap();
163 doc.content = "Test content".to_string();
164
165 store.save(&doc).unwrap();
166
167 assert!(doc.path.exists());
169 assert_eq!(fs::read_to_string(&doc.path).unwrap(), "Test content");
170
171 fs::remove_dir_all(&temp_dir).ok();
173 }
174
175 #[test]
176 fn test_list_all_documents_recursive() {
177 let temp_dir = env::temp_dir().join("piki-test-list-all");
178 let _ = fs::remove_dir_all(&temp_dir);
179 fs::create_dir_all(&temp_dir).unwrap();
180
181 let store = DocumentStore::new(temp_dir.clone());
182
183 fs::write(temp_dir.join("root.md"), "root").unwrap();
185 fs::create_dir_all(temp_dir.join("dir1")).unwrap();
186 fs::write(temp_dir.join("dir1/page1.md"), "page1").unwrap();
187 fs::create_dir_all(temp_dir.join("dir1/subdir")).unwrap();
188 fs::write(temp_dir.join("dir1/subdir/page2.md"), "page2").unwrap();
189
190 let docs = store.list_all_documents().unwrap();
191
192 assert!(docs.contains(&"root".to_string()));
193 assert!(docs.contains(&"dir1/page1".to_string()));
194 assert!(docs.contains(&"dir1/subdir/page2".to_string()));
195 assert_eq!(docs.len(), 3);
196
197 fs::remove_dir_all(&temp_dir).ok();
199 }
200}