use std::ffi::OsStr;
use std::fs::{self, create_dir_all, remove_dir_all};
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use crate::util::{decode_folder_name, encode_folder_name};
use crate::{Error, M2dir};
#[derive(Clone, Debug)]
pub struct M2store {
root: PathBuf,
}
impl TryFrom<&Path> for M2store {
type Error = Error;
fn try_from(path: &Path) -> Result<Self, Error> {
let path = path.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 })
}
}
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> {
M2store::try_from(path.as_ref())
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn folders(&self) -> Folders {
Folders::new(self.root.clone(), false)
}
pub fn all_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: &str) -> Result<Folder, Error> {
if folder.starts_with('.') || folder.chars().any(|c| c.is_ascii_control()) {
return Err(Error::InvalidFolderName(folder.to_string()));
}
let path = encode_folder_name(folder);
let mut full_path = self.root.clone();
full_path.push(path.as_ref());
let m2dir = M2dir::create(&full_path)?;
Ok(Folder::new(m2dir, PathBuf::from(path.as_ref())))
}
pub fn create_folders(&self, folder: &[impl AsRef<str>]) -> Result<Folder, Error> {
let mut r: Option<Folder> = None;
for f in folder {
if let Some(r_) = r {
r = Some(r_.create_subfolder(f.as_ref())?);
} else {
r = Some(self.create_folder(f.as_ref())?);
}
}
r.ok_or(Error::InvalidFolderName("no folders provided".to_string()))
}
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::new(m2dir, name)))
});
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,
names: Vec<String>,
}
impl Folder {
fn new(m2dir: M2dir, path: PathBuf) -> Self {
let names = path
.components()
.map(|c| decode_folder_name(&c.as_os_str().to_string_lossy()).to_string())
.collect();
Folder { m2dir, path, names }
}
pub fn abs_path(&self) -> &PathBuf {
&self.m2dir.path
}
pub fn path(&self) -> &PathBuf {
&self.path
}
pub fn name(&self) -> &str {
self.names.last().expect("folder has no names")
}
pub fn names(&self) -> &[String] {
&self.names
}
pub fn subfolders(&self) -> Folders {
Folders::new_sub(self.path.clone(), self.m2dir.path.clone(), false)
}
pub fn all_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() == name))
.transpose()
}
pub fn create_subfolder(&self, folder: &str) -> Result<Folder, Error> {
if folder.starts_with('.') {
return Err(Error::InvalidFolderName(folder.to_string()));
}
let name = encode_folder_name(folder);
let mut path = self.path().to_path_buf();
path.push(name.as_ref());
let mut full_path = self.m2dir.path().to_path_buf();
full_path.push(name.as_ref());
let m2dir = M2dir::create(&full_path)?;
Ok(Folder::new(m2dir, path))
}
}
#[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.all_folders().count(), 5);
let mut names: Vec<String> = m2store
.all_folders()
.map(|f| f.unwrap().name().to_string())
.collect();
names.sort();
assert_eq!(
names,
vec![
"INBOX",
"brokenflags",
"folder",
"lists/m2dir-dev",
"subfölder",
]
);
assert_eq!(m2store.folders().count(), 4);
let mut names: Vec<String> = m2store
.folders()
.map(|f| f.unwrap().name().to_string())
.collect();
names.sort();
assert_eq!(
names,
vec!["INBOX", "brokenflags", "folder", "lists/m2dir-dev"]
);
let folder = m2store.folder("INBOX").unwrap().unwrap();
assert_eq!(folder.name(), "INBOX".to_string());
assert_eq!(folder.names(), ["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.names(), ["folder".to_string()]);
let m2dir: &M2dir = folder.as_ref();
assert_eq!(m2dir.count(), 1);
assert_eq!(folder.subfolders().count(), 1);
let sub = folder.subfolder("subfölder").unwrap().unwrap();
assert_eq!(sub.name(), "subfölder".to_string());
assert_eq!(sub.names(), ["folder".to_string(), "subfölder".to_string()]);
assert_eq!(sub.path().to_str(), Some("folder/subfölder"));
let m2dir: &M2dir = sub.as_ref();
assert_eq!(m2dir.count(), 1);
let new = folder.create_subfolder("new").unwrap();
assert_eq!(new.name(), "new".to_string());
assert_eq!(new.names(), ["folder".to_string(), "new".to_string()]);
assert_eq!(new.path().to_str(), Some("folder/new"));
let m2dir: &M2dir = new.as_ref();
assert_eq!(m2dir.count(), 0);
let folder = m2store.folder("lists/m2dir-dev").unwrap().unwrap();
assert_eq!(folder.name(), "lists/m2dir-dev".to_string());
assert_eq!(folder.names(), ["lists/m2dir-dev".to_string()]);
assert_eq!(folder.path().to_str(), Some("lists%2Fm2dir-dev"));
}
}