1use std::ffi::OsStr;
2use std::fs::{self, create_dir_all, remove_dir_all};
3use std::path::{Path, PathBuf};
4
5use walkdir::WalkDir;
6
7use crate::{Error, M2dir};
8
9#[derive(Clone, Debug)]
14pub struct M2store {
15 root: PathBuf,
16}
17
18impl M2store {
19 pub fn create(root: impl AsRef<Path>) -> Result<M2store, Error> {
21 if !root.as_ref().exists() {
22 create_dir_all(&root)?;
23 }
24 let marker = PathBuf::from_iter([root.as_ref().as_os_str(), OsStr::new(".m2store")]);
25 let _ = fs::File::create(marker)?;
26 M2store::open(&root)
27 }
28
29 pub fn open(path: impl AsRef<Path>) -> Result<M2store, Error> {
31 let path = path.as_ref().canonicalize()?;
32 let marker = PathBuf::from_iter([path.as_os_str(), OsStr::new(".m2store")]);
33 if !marker.is_file() {
35 return Err(Error::FolderNotFound);
37 }
38 Ok(M2store { root: path })
39 }
40
41 pub fn root(&self) -> &PathBuf {
42 &self.root
43 }
44
45 pub fn folders(&self) -> Folders {
46 Folders::new(self.root.clone(), true)
47 }
48
49 pub fn folder(&self, name: &str) -> Result<Option<Folder>, Error> {
50 self.folders()
51 .find(|f| f.as_ref().is_ok_and(|f| f.name() == name))
52 .transpose()
53 }
54
55 pub fn create_folder(&self, folder: impl AsRef<Path>) -> Result<Folder, Error> {
56 let path = folder.as_ref();
58 if path.is_absolute() {
59 return Err(Error::InvalidFolderName(String::from(
60 path.to_string_lossy(),
61 )));
62 }
63 let mut full_path = self.root.clone();
64 full_path.push(path);
65 let m2dir = M2dir::create(&full_path)?;
66 Ok(Folder {
67 path: PathBuf::from(&path),
68 m2dir,
69 })
70 }
71
72 pub fn delete_folder(&self, folder: Folder) -> Result<(), Error> {
73 remove_dir_all(folder.abs_path())?;
74 Ok(())
75 }
76}
77
78pub struct Folders {
85 name: PathBuf,
86 path: PathBuf,
87 walkdir: Option<walkdir::IntoIter>,
88 include_self: bool,
89 recurse: bool,
90}
91
92impl Folders {
93 fn new(path: PathBuf, recurse: bool) -> Folders {
95 Folders {
96 name: PathBuf::from(""),
97 path,
98 walkdir: None,
99 include_self: true,
100 recurse,
101 }
102 }
103
104 fn new_sub(name: PathBuf, path: PathBuf, recurse: bool) -> Folders {
105 Folders {
106 name,
107 path,
108 walkdir: None,
109 include_self: false,
110 recurse,
111 }
112 }
113}
114
115impl AsRef<M2dir> for Folder {
116 fn as_ref(&self) -> &M2dir {
117 &self.m2dir
118 }
119}
120
121impl Iterator for Folders {
122 type Item = Result<Folder, Error>;
123
124 fn next(&mut self) -> Option<Result<Folder, Error>> {
125 if self.walkdir.is_none() {
126 let max_depth = if self.recurse { usize::MAX } else { 1 };
127 let min_depth = if self.include_self { 0 } else { 1 };
128 self.walkdir = Some(
129 WalkDir::new(&self.path)
130 .min_depth(min_depth)
131 .max_depth(max_depth)
132 .into_iter(),
133 );
134 }
135
136 loop {
137 let dir_entry = self.walkdir.as_mut().unwrap().next();
138 let result = dir_entry.map(|e| {
139 let entry = e?;
140 let dot = b'.';
141 let first_char = entry.file_name().as_encoded_bytes().first().unwrap_or(&dot);
142 if *first_char == dot && entry.path() != self.path {
145 return Ok(None);
146 }
147
148 let is_dir = entry.metadata().map(|m| m.is_dir()).unwrap_or(false);
150 if !is_dir {
151 return Ok(None);
152 }
153
154 let name = if entry.path() != self.path {
155 let rel = entry.path().strip_prefix(&self.path).unwrap();
156 PathBuf::from_iter([self.name.as_path(), rel])
157 } else {
158 PathBuf::from(".")
159 };
160
161 let m2dir = match M2dir::try_from(self.path.join(entry.path()).as_ref()) {
162 Ok(m2dir) => m2dir,
163 Err(_) => return Ok(None),
164 };
165
166 Ok(Some(Folder { path: name, m2dir }))
167 });
168
169 return match result {
170 None => None,
171 Some(Err(e)) => Some(Err(e)),
172 Some(Ok(None)) => continue,
173 Some(Ok(Some(v))) => Some(Ok(v)),
174 };
175 }
176 }
177}
178
179#[derive(Clone, Debug, PartialEq, Eq)]
181pub struct Folder {
182 m2dir: M2dir,
183 path: PathBuf,
184}
185
186impl Folder {
187 pub fn abs_path(&self) -> &PathBuf {
188 &self.m2dir.path
189 }
190
191 pub fn path(&self) -> &PathBuf {
192 &self.path
193 }
194
195 pub fn name(&self) -> String {
196 self.path.to_string_lossy().to_string()
197 }
198
199 pub fn virtual_name(&self, delimiter: &str) -> String {
200 let v: Vec<_> = self
201 .path
202 .components()
203 .map(|c| c.as_os_str().to_string_lossy().to_string())
204 .collect();
205 v.join(delimiter)
206 }
207
208 pub fn subfolders(&self) -> Folders {
210 Folders::new_sub(self.path.clone(), self.m2dir.path.clone(), true)
211 }
212
213 pub fn subfolder(&self, name: &str) -> Result<Option<Folder>, Error> {
214 self.subfolders()
215 .find(|f| {
216 f.as_ref()
217 .is_ok_and(|f| f.name() == format!("{}/{}", self.name(), name))
218 })
219 .transpose()
220 }
221
222 pub fn add_subfolder(&self, folder: impl AsRef<Path>) -> Result<Folder, Error> {
225 let path = folder.as_ref();
226 if path.is_absolute() {
227 return Err(Error::InvalidFolderName(String::from(
228 path.to_string_lossy(),
229 )));
230 }
231 let mut abs_path = self.m2dir.path.clone();
232 abs_path.push(path);
233 let mut name = self.path.clone();
234 name.push(path);
235 fs::create_dir_all(&abs_path)?;
236 Ok(Folder {
237 m2dir: M2dir { path: abs_path },
238 path: name,
239 })
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use tempfile::{tempdir, TempDir};
246
247 use super::*;
248
249 fn setup(dir: &TempDir) {
250 crate::tests::generate_test_data(dir.path());
251 }
252
253 #[test]
254 fn test_load() {
255 let tmpdir = tempdir().unwrap();
256 setup(&tmpdir);
257
258 let m2store = M2store::create(tmpdir.path()).unwrap();
259 assert_eq!(m2store.folders().count(), 5);
260
261 let mut names: Vec<String> = m2store.folders().map(|f| f.unwrap().name()).collect();
262 names.sort();
263 assert_eq!(
265 names,
266 vec![
267 "INBOX",
268 "brokenflags",
269 "folder",
270 "folder/subfolder",
271 "lists/m2dir-dev"
272 ]
273 );
274
275 let folder = m2store.folder("INBOX").unwrap().unwrap();
276 assert_eq!(folder.name(), "INBOX".to_string());
277 assert_eq!(folder.virtual_name("."), "INBOX".to_string());
278 let m2dir: &M2dir = folder.as_ref();
279 assert_eq!(m2dir.count(), 5);
280
281 let folder = m2store.folder("folder").unwrap().unwrap();
282 assert_eq!(folder.name(), "folder".to_string());
283 assert_eq!(folder.virtual_name("."), "folder".to_string());
284 let m2dir: &M2dir = folder.as_ref();
285 assert_eq!(m2dir.count(), 1);
286
287 assert_eq!(folder.subfolders().count(), 1);
288
289 let sub = folder.subfolder("subfolder").unwrap().unwrap();
290 assert_eq!(sub.name(), "folder/subfolder".to_string());
291 assert_eq!(sub.virtual_name("."), "folder.subfolder".to_string());
292 let m2dir: &M2dir = sub.as_ref();
293 assert_eq!(m2dir.count(), 1);
294 }
295}