use std::ffi::OsStr;
use std::fs::{self, create_dir_all, remove_dir_all};
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use crate::{Error, M2dir};
#[derive(Clone, Debug)]
pub struct M2store {
root: PathBuf,
}
impl M2store {
pub fn create(root: impl AsRef<Path>) -> Result<M2store, Error> {
if !root.as_ref().exists() {
create_dir_all(&root)?;
}
let marker = PathBuf::from_iter([root.as_ref().as_os_str(), OsStr::new(".m2store")]);
let _ = fs::File::create(marker)?;
M2store::open(&root)
}
pub fn open(path: impl AsRef<Path>) -> Result<M2store, Error> {
let path = path.as_ref().canonicalize()?;
let marker = PathBuf::from_iter([path.as_os_str(), OsStr::new(".m2store")]);
if !marker.is_file() {
return Err(Error::FolderNotFound);
}
Ok(M2store { root: path })
}
pub fn root(&self) -> &PathBuf {
&self.root
}
pub fn folders(&self) -> Folders {
Folders::new(self.root.clone(), true)
}
pub fn folder(&self, name: &str) -> Result<Option<Folder>, Error> {
self.folders()
.find(|f| f.as_ref().is_ok_and(|f| f.name() == name))
.transpose()
}
pub fn create_folder(&self, folder: impl AsRef<Path>) -> Result<Folder, Error> {
let path = folder.as_ref();
if path.is_absolute() {
return Err(Error::InvalidFolderName(String::from(
path.to_string_lossy(),
)));
}
let mut full_path = self.root.clone();
full_path.push(path);
let m2dir = M2dir::create(&full_path)?;
Ok(Folder {
path: PathBuf::from(&path),
m2dir,
})
}
pub fn delete_folder(&self, folder: Folder) -> Result<(), Error> {
remove_dir_all(folder.abs_path())?;
Ok(())
}
}
pub struct Folders {
name: PathBuf,
path: PathBuf,
walkdir: Option<walkdir::IntoIter>,
include_self: bool,
recurse: bool,
}
impl Folders {
fn new(path: PathBuf, recurse: bool) -> Folders {
Folders {
name: PathBuf::from(""),
path,
walkdir: None,
include_self: true,
recurse,
}
}
fn new_sub(name: PathBuf, path: PathBuf, recurse: bool) -> Folders {
Folders {
name,
path,
walkdir: None,
include_self: false,
recurse,
}
}
}
impl AsRef<M2dir> for Folder {
fn as_ref(&self) -> &M2dir {
&self.m2dir
}
}
impl Iterator for Folders {
type Item = Result<Folder, Error>;
fn next(&mut self) -> Option<Result<Folder, Error>> {
if self.walkdir.is_none() {
let max_depth = if self.recurse { usize::MAX } else { 1 };
let min_depth = if self.include_self { 0 } else { 1 };
self.walkdir = Some(
WalkDir::new(&self.path)
.min_depth(min_depth)
.max_depth(max_depth)
.into_iter(),
);
}
loop {
let dir_entry = self.walkdir.as_mut().unwrap().next();
let result = dir_entry.map(|e| {
let entry = e?;
let dot = b'.';
let first_char = entry.file_name().as_encoded_bytes().first().unwrap_or(&dot);
if *first_char == dot && entry.path() != self.path {
return Ok(None);
}
let is_dir = entry.metadata().map(|m| m.is_dir()).unwrap_or(false);
if !is_dir {
return Ok(None);
}
let name = if entry.path() != self.path {
let rel = entry.path().strip_prefix(&self.path).unwrap();
PathBuf::from_iter([self.name.as_path(), rel])
} else {
PathBuf::from(".")
};
let m2dir = match M2dir::try_from(self.path.join(entry.path()).as_ref()) {
Ok(m2dir) => m2dir,
Err(_) => return Ok(None),
};
Ok(Some(Folder { path: name, m2dir }))
});
return match result {
None => None,
Some(Err(e)) => Some(Err(e)),
Some(Ok(None)) => continue,
Some(Ok(Some(v))) => Some(Ok(v)),
};
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Folder {
m2dir: M2dir,
path: PathBuf,
}
impl Folder {
pub fn abs_path(&self) -> &PathBuf {
&self.m2dir.path
}
pub fn path(&self) -> &PathBuf {
&self.path
}
pub fn name(&self) -> String {
self.path.to_string_lossy().to_string()
}
pub fn virtual_name(&self, delimiter: &str) -> String {
let v: Vec<_> = self
.path
.components()
.map(|c| c.as_os_str().to_string_lossy().to_string())
.collect();
v.join(delimiter)
}
pub fn subfolders(&self) -> Folders {
Folders::new_sub(self.path.clone(), self.m2dir.path.clone(), true)
}
pub fn subfolder(&self, name: &str) -> Result<Option<Folder>, Error> {
self.subfolders()
.find(|f| {
f.as_ref()
.is_ok_and(|f| f.name() == format!("{}/{}", self.name(), name))
})
.transpose()
}
pub fn add_subfolder(&self, folder: impl AsRef<Path>) -> Result<Folder, Error> {
let path = folder.as_ref();
if path.is_absolute() {
return Err(Error::InvalidFolderName(String::from(
path.to_string_lossy(),
)));
}
let mut abs_path = self.m2dir.path.clone();
abs_path.push(path);
let mut name = self.path.clone();
name.push(path);
fs::create_dir_all(&abs_path)?;
Ok(Folder {
m2dir: M2dir { path: abs_path },
path: name,
})
}
}
#[cfg(test)]
mod tests {
use tempfile::{tempdir, TempDir};
use super::*;
fn setup(dir: &TempDir) {
crate::tests::generate_test_data(dir.path());
}
#[test]
fn test_load() {
let tmpdir = tempdir().unwrap();
setup(&tmpdir);
let m2store = M2store::create(tmpdir.path()).unwrap();
assert_eq!(m2store.folders().count(), 5);
let mut names: Vec<String> = m2store.folders().map(|f| f.unwrap().name()).collect();
names.sort();
assert_eq!(
names,
vec![
"INBOX",
"brokenflags",
"folder",
"folder/subfolder",
"lists/m2dir-dev"
]
);
let folder = m2store.folder("INBOX").unwrap().unwrap();
assert_eq!(folder.name(), "INBOX".to_string());
assert_eq!(folder.virtual_name("."), "INBOX".to_string());
let m2dir: &M2dir = folder.as_ref();
assert_eq!(m2dir.count(), 5);
let folder = m2store.folder("folder").unwrap().unwrap();
assert_eq!(folder.name(), "folder".to_string());
assert_eq!(folder.virtual_name("."), "folder".to_string());
let m2dir: &M2dir = folder.as_ref();
assert_eq!(m2dir.count(), 1);
assert_eq!(folder.subfolders().count(), 1);
let sub = folder.subfolder("subfolder").unwrap().unwrap();
assert_eq!(sub.name(), "folder/subfolder".to_string());
assert_eq!(sub.virtual_name("."), "folder.subfolder".to_string());
let m2dir: &M2dir = sub.as_ref();
assert_eq!(m2dir.count(), 1);
}
}