m2dir/
store.rs

1use std::{
2    error::Error,
3    ffi::OsStr,
4    fmt::{Debug, Display},
5    fs::{self, create_dir_all},
6    io::Read,
7    os::unix::ffi::OsStrExt,
8    path::{Component, Path, PathBuf},
9};
10
11use crate::{
12    error::LoadM2DirError,
13    fs::is_dotfile,
14    percent::{percent_decode_bytes, percent_encode_bytes},
15    walk::Walker,
16    Entry, M2Dir,
17};
18
19/// Represents an m2store directory structure.
20pub struct M2Store {
21    path: PathBuf,
22}
23
24impl M2Store {
25    /// Returns the path to the m2store root directory.
26    pub fn path(&self) -> &Path {
27        &self.path
28    }
29
30    /// Determines the appropriate [`M2Dir`] for delivering new messages.
31    pub fn delivery_target(&self) -> Result<M2Dir, DeliveryError> {
32        let delivery = self.path.join(".delivery");
33
34        if delivery.exists() {
35            if let Ok(target) = fs::read_link(&delivery) {
36                return Ok(M2Dir::new(self.path.join(target))?);
37            }
38
39            if delivery.is_file() {
40                let target = self.path.join(fs::read_to_string(&delivery)?.trim());
41                return Ok(M2Dir::new(target)?);
42            }
43        }
44
45        Err(DeliveryError::UnspecifiedTarget)
46    }
47
48    /// Deliver a new message to the m2store's delivery target
49    ///
50    /// The delivery target will be resolved using the `.delivery` file and the
51    /// message will be stored in the target m2dir.
52    ///
53    /// # Errors
54    ///
55    /// This function will error if the delivery target is not a valid m2dir.
56    /// Additionally, if any io errors arise while storing the message, they
57    /// will also get thrown.
58    pub fn deliver<R: Read>(&self, reader: R) -> Result<Entry, DeliveryError> {
59        Ok(self.delivery_target()?.store(reader)?)
60    }
61
62    /// Creates a new folder within the m2store.
63    ///
64    /// The given path has to be relative, and the created folder will be
65    /// relative to the root of the m2store. The function returns the created
66    /// [`M2Dir`].
67    ///
68    /// # Errors
69    ///
70    /// If the given path is absolute, an error is returned. An error is also
71    /// returned if the directory creation failed.
72    pub fn new_folder<P: AsRef<Path> + Debug>(&self, path: P) -> Result<M2Dir, NewFolderError<P>> {
73        let mut encoded_path = self.path.clone();
74        for component in path.as_ref().components() {
75            match component {
76                Component::Prefix(_) | Component::RootDir => {
77                    return Err(NewFolderError::AbsolutePath(path))
78                }
79                Component::Normal(s) => encoded_path.push(self.encode_folder_name(s)),
80                _ => {}
81            }
82        }
83        create_dir_all(&encoded_path)?;
84        Ok(M2Dir { path: encoded_path })
85    }
86
87    /// Returns an iterator over folders in the m2store.
88    ///
89    /// The decoded names and the corresponding [`M2Dir`] are returned,
90    /// respectively.
91    pub fn folders(&self) -> impl Iterator<Item = (String, M2Dir)> + '_ {
92        Walker::new(self.path())
93            .unwrap_or_default()
94            .filter(|path| path.is_dir())
95            .filter(|path| !is_dotfile(path))
96            .filter_map(|path| Some((self.decode_folder_name(&path), M2Dir::try_from(path).ok()?)))
97    }
98
99    /// Returns an iterator over the m2dir objects in the m2store.
100    ///
101    /// Only the [`M2Dir`] are returned by this iterator. See
102    /// [`M2Store::folders`] for an iterator that returns both the decoded
103    /// name and [`M2Dir`] objects.
104    pub fn m2dirs(&self) -> impl Iterator<Item = M2Dir> + '_ {
105        self.folders().map(|(_, m2dir)| m2dir)
106    }
107
108    /// Returns an iterator over the decoded names of the folders in the
109    /// m2store.
110    ///
111    /// Only the decoded names are returned by this iterator. See
112    /// [`M2Store::folders`] for an iterator that returns both the decoded names
113    /// and [`M2Dir`] objects.
114    pub fn folder_names(&self) -> impl Iterator<Item = String> + '_ {
115        self.folders().map(|(name, _)| name)
116    }
117
118    fn decode_folder_name(&self, path: &Path) -> String {
119        // FIXME: If percent decoding yields invalid UTF-8, this will panic
120        percent_decode_bytes(path.to_string_lossy().bytes())
121            .expect("Decoding folder name failed: Percent encoding yields invalid UTF-8")
122    }
123
124    fn encode_folder_name(&self, name: &OsStr) -> PathBuf {
125        let mut path = String::new();
126        // FIXME: If OsStr is not UTF-8, this might panic
127        percent_encode_bytes(name.as_bytes(), &mut path)
128            .expect("Folder name encoding failed: OsStr not UTF-8");
129        PathBuf::from(path)
130    }
131}
132
133impl TryFrom<&Path> for M2Store {
134    type Error = LoadM2StoreError;
135
136    fn try_from(path: &Path) -> Result<Self, Self::Error> {
137        Self::try_from(path.to_path_buf())
138    }
139}
140
141impl TryFrom<PathBuf> for M2Store {
142    type Error = LoadM2StoreError;
143
144    fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
145        if !path.is_dir() {
146            return Err(LoadM2StoreError::NotDir);
147        }
148
149        if !path.join(".m2store").exists() {
150            return Err(LoadM2StoreError::NoDotM2Store);
151        }
152
153        Ok(Self { path })
154    }
155}
156
157/// An error that can occur while loading an m2store
158#[derive(Debug)]
159pub enum LoadM2StoreError {
160    /// The given path was not a directory
161    NotDir,
162    /// The given path does not contain a valid `.m2store` file
163    NoDotM2Store,
164}
165
166impl Display for LoadM2StoreError {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        match self {
169            Self::NotDir => write!(f, "is not a directory"),
170            Self::NoDotM2Store => write!(f, "no valid `.m2store` found in directory"),
171        }
172    }
173}
174
175impl Error for LoadM2StoreError {}
176
177/// An error that can occur during delivery.
178#[derive(Debug)]
179pub enum DeliveryError {
180    /// Represents an I/O error that occurred during the delivery process.
181    Io(std::io::Error),
182    /// Indicates that the targetted m2dir is invalid
183    InvalidTarget(LoadM2DirError),
184    /// Represents the absence of a `.delivery` entry in an m2store, required to
185    /// determine where the delivery target is.
186    UnspecifiedTarget,
187}
188
189impl Display for DeliveryError {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        match self {
192            Self::Io(e) => write!(f, "{e}"),
193            Self::InvalidTarget(e) => write!(f, "{e}"),
194            Self::UnspecifiedTarget => write!(f, "could not find `.delivery` in m2store"),
195        }
196    }
197}
198
199impl Error for DeliveryError {}
200
201impl From<std::io::Error> for DeliveryError {
202    fn from(e: std::io::Error) -> Self {
203        Self::Io(e)
204    }
205}
206
207impl From<LoadM2DirError> for DeliveryError {
208    fn from(e: LoadM2DirError) -> Self {
209        Self::InvalidTarget(e)
210    }
211}
212
213/// An error that can occur while creating a folder in an m2store
214#[derive(Debug)]
215pub enum NewFolderError<P: AsRef<Path> + Debug> {
216    /// The given path was absolute, which is invalid when creating a folder
217    AbsolutePath(P),
218    /// Represents an I/O error that occurred during the creation of the folder.
219    Io(std::io::Error),
220}
221
222impl<P: AsRef<Path> + Debug> Error for NewFolderError<P> {}
223
224impl<P: AsRef<Path> + Debug> Display for NewFolderError<P> {
225    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226        match self {
227            Self::Io(e) => write!(f, "{e}"),
228            Self::AbsolutePath(path) => write!(f, "Path {:?} has to be relative", path.as_ref()),
229        }
230    }
231}
232
233impl<P: AsRef<Path> + Debug> From<std::io::Error> for NewFolderError<P> {
234    fn from(e: std::io::Error) -> Self {
235        Self::Io(e)
236    }
237}