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
19pub struct M2Store {
21 path: PathBuf,
22}
23
24impl M2Store {
25 pub fn path(&self) -> &Path {
27 &self.path
28 }
29
30 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 pub fn deliver<R: Read>(&self, reader: R) -> Result<Entry, DeliveryError> {
59 Ok(self.delivery_target()?.store(reader)?)
60 }
61
62 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 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 pub fn m2dirs(&self) -> impl Iterator<Item = M2Dir> + '_ {
105 self.folders().map(|(_, m2dir)| m2dir)
106 }
107
108 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 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 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#[derive(Debug)]
159pub enum LoadM2StoreError {
160 NotDir,
162 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#[derive(Debug)]
179pub enum DeliveryError {
180 Io(std::io::Error),
182 InvalidTarget(LoadM2DirError),
184 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#[derive(Debug)]
215pub enum NewFolderError<P: AsRef<Path> + Debug> {
216 AbsolutePath(P),
218 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}