vomit_m2dir/
m2store.rs

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/// An m2store as defined in the m2dir spec.
10///
11/// Any instance created by this implementation is guaranteed to be an existing
12/// directory with a `.m2dir.root` marker file.
13#[derive(Clone, Debug)]
14pub struct M2store {
15    root: PathBuf,
16}
17
18impl M2store {
19    /// Create a new m2store.
20    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    /// Open an existing m2store.
30    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        // TODO check for marker file
34        if !marker.is_file() {
35            // TODO better error message
36            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        // TODO canonicalize or otherwise prevent "../"?
57        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
78/// An iterator over [`Folder`]s in an [`M2store`].
79///
80/// The order of subdirectories in the iterator is not specified, and is not
81/// guaranteed to be stable over multiple invocations of this method. However,
82/// child directories are guaranteed to be listed after their parent
83/// directories.
84pub 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    // TODO: remove recurse?
94    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                // The root directory (self.path) is an exception insofar that
143                // it may begin with a '.' - think ~/.mail - but must be included (if it is an m2dir)
144                if *first_char == dot && entry.path() != self.path {
145                    return Ok(None);
146                }
147
148                // the entry must be a directory
149                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/// A folder is an [`M2dir`] embedded in the context of an [`M2store`].
180#[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    /// Returns an iterator over subdirectories.
209    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    /// Creates all neccessary directories if they don't exist yet. It is the library user's
223    /// responsibility to call this when e.g. creating a new folder.
224    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        // lists does not show up because it is not an m2dir itself
264        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}