1use core::{fmt, mem};
55
56use alloc::{
57 collections::BTreeSet,
58 string::{String, ToString},
59 vec::Vec,
60};
61
62use log::trace;
63use thiserror::Error;
64
65use crate::{
66 coroutine::*, entry::list::*, entry::types::M2dirEntry, m2dir::types::M2dir, path::M2dirPath,
67};
68
69#[derive(Clone, Debug, Error)]
71pub enum M2dirEntryDeleteError {
72 #[error("M2DIR DELETE failed: unexpected coroutine arg")]
73 UnexpectedArg,
74 #[error("M2DIR DELETE failed: missing coroutine arg")]
75 MissingArg,
76 #[error("M2DIR DELETE failed: entry {0} not found")]
77 NotFound(String),
78 #[error("M2DIR DELETE failed: {0}")]
79 List(#[from] M2dirEntryListError),
80}
81
82#[derive(Clone, Debug, Default, Eq, PartialEq)]
84pub struct M2dirEntryDeleteOptions {}
85
86pub struct M2dirEntryDelete {
88 id: String,
89 meta_dir: M2dirPath,
90 state: State,
91 #[allow(dead_code)]
92 opts: M2dirEntryDeleteOptions,
93}
94
95impl M2dirEntryDelete {
96 pub fn new(m2dir: M2dir, id: impl ToString, opts: M2dirEntryDeleteOptions) -> Self {
99 let meta_dir = m2dir.meta_dir();
100 Self {
101 id: id.to_string(),
102 meta_dir,
103 state: State::List(M2dirEntryList::new(m2dir, M2dirEntryListOptions::default())),
104 opts,
105 }
106 }
107}
108
109impl M2dirCoroutine for M2dirEntryDelete {
110 type Yield = M2dirYield;
111 type Return = Result<(), M2dirEntryDeleteError>;
112
113 fn resume(&mut self, arg: Option<M2dirArg>) -> M2dirCoroutineState<Self::Yield, Self::Return> {
114 trace!("delete entry: {}", self.state);
115
116 match (&mut self.state, arg) {
117 (State::List(list), arg) => match list.resume(arg) {
118 M2dirCoroutineState::Yielded(yld) => M2dirCoroutineState::Yielded(yld),
119 M2dirCoroutineState::Complete(Ok(entries)) => {
120 let Some(entry) = entries.into_iter().find(|e| e.id() == self.id) else {
121 let err = M2dirEntryDeleteError::NotFound(self.id.clone());
122 return M2dirCoroutineState::Complete(Err(err));
123 };
124
125 trace!("located entry at {}", entry.path());
126
127 let paths = BTreeSet::from_iter([self.meta_dir.clone()]);
128 self.state = State::ReadMeta(entry);
129 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(paths))
130 }
131 M2dirCoroutineState::Complete(Err(err)) => {
132 M2dirCoroutineState::Complete(Err(err.into()))
133 }
134 },
135 (State::ReadMeta(entry), Some(M2dirArg::DirRead(entries))) => {
136 let meta_names = entries.into_values().next().unwrap_or_default();
137 let entry = mem::replace(
138 entry,
139 M2dirEntry::from_parts(String::new(), M2dirPath::default()),
140 );
141
142 let mut to_remove: Vec<M2dirPath> = Vec::new();
143 to_remove.push(entry.path().clone());
144
145 for path in meta_names {
146 let Some(name) = path.file_name() else {
147 continue;
148 };
149 if name.starts_with(&self.id) {
150 to_remove.push(path);
151 }
152 }
153
154 trace!("wants removal of {} files", to_remove.len());
155 self.state = State::Removing;
156 M2dirCoroutineState::Yielded(M2dirYield::WantsFileRemove(BTreeSet::from_iter(
157 to_remove,
158 )))
159 }
160 (State::Removing, Some(M2dirArg::FileRemove)) => {
161 trace!("entry deleted");
162 M2dirCoroutineState::Complete(Ok(()))
163 }
164 (_, Some(_)) => {
165 let err = M2dirEntryDeleteError::UnexpectedArg;
166 M2dirCoroutineState::Complete(Err(err))
167 }
168 (_, None) => {
169 let err = M2dirEntryDeleteError::MissingArg;
170 M2dirCoroutineState::Complete(Err(err))
171 }
172 }
173 }
174}
175
176enum State {
177 List(M2dirEntryList),
178 ReadMeta(M2dirEntry),
179 Removing,
180}
181
182impl fmt::Display for State {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 match self {
185 Self::List(_) => f.write_str("locate entry"),
186 Self::ReadMeta(_) => f.write_str("read .meta directory"),
187 Self::Removing => f.write_str("removing files"),
188 }
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use alloc::collections::BTreeMap;
195
196 use super::*;
197
198 #[test]
199 fn missing_entry_returns_not_found_error() {
200 let m2dir = M2dir::from_path("/tmp/inbox");
201 let mut delete =
202 M2dirEntryDelete::new(m2dir, "missing", M2dirEntryDeleteOptions::default());
203
204 let probes = match delete.resume(None) {
205 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(paths)) => paths,
206 state => panic!("expected WantsDirRead, got {state:?}"),
207 };
208 let dir = probes.into_iter().next().unwrap();
209 let mut reply = BTreeMap::new();
210 reply.insert(dir, BTreeSet::new());
211
212 let err = match delete.resume(Some(M2dirArg::DirRead(reply))) {
213 M2dirCoroutineState::Complete(Err(err)) => err,
214 state => panic!("expected Complete(Err), got {state:?}"),
215 };
216 assert!(matches!(err, M2dirEntryDeleteError::NotFound(id) if id == "missing"));
217 }
218
219 #[test]
220 fn list_error_propagates_via_from() {
221 let m2dir = M2dir::from_path("/tmp/inbox");
222 let mut delete = M2dirEntryDelete::new(m2dir, "x", M2dirEntryDeleteOptions::default());
223
224 let _ = delete.resume(None);
225 let err = match delete.resume(Some(M2dirArg::FileCreate)) {
226 M2dirCoroutineState::Complete(Err(err)) => err,
227 state => panic!("expected Complete(Err), got {state:?}"),
228 };
229 assert!(matches!(err, M2dirEntryDeleteError::List(_)));
230 }
231
232 #[test]
233 fn unexpected_arg_at_list_initial_returns_list_error() {
234 let m2dir = M2dir::from_path("/tmp/inbox");
235 let mut delete = M2dirEntryDelete::new(m2dir, "x", M2dirEntryDeleteOptions::default());
236
237 let err = match delete.resume(Some(M2dirArg::FileRead(BTreeMap::new()))) {
238 M2dirCoroutineState::Complete(Err(err)) => err,
239 state => panic!("expected Complete(Err), got {state:?}"),
240 };
241 assert!(matches!(err, M2dirEntryDeleteError::List(_)));
242 }
243
244 #[test]
245 fn missing_arg_at_list_propagates_via_list_error() {
246 let m2dir = M2dir::from_path("/tmp/inbox");
247 let mut delete = M2dirEntryDelete::new(m2dir, "x", M2dirEntryDeleteOptions::default());
248 let _ = delete.resume(None);
249
250 let err = match delete.resume(None) {
251 M2dirCoroutineState::Complete(Err(err)) => err,
252 state => panic!("expected Complete(Err), got {state:?}"),
253 };
254 assert!(matches!(err, M2dirEntryDeleteError::List(_)));
255 }
256
257 #[test]
258 fn read_meta_unexpected_arg_returns_unexpected_arg_error() {
259 let m2dir = M2dir::from_path("/tmp/inbox");
260 let mut delete = M2dirEntryDelete::new(m2dir, "any", M2dirEntryDeleteOptions::default());
261
262 let probes = match delete.resume(None) {
263 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(paths)) => paths,
264 state => panic!("expected WantsDirRead, got {state:?}"),
265 };
266 let dir = probes.into_iter().next().unwrap();
267 let mut names = BTreeSet::new();
268 let entry_path = M2dirPath::from("/tmp/inbox/123,checksum.nonce");
269 names.insert(entry_path.clone());
270 let mut reply = BTreeMap::new();
271 reply.insert(dir, names);
272
273 let probes = match delete.resume(Some(M2dirArg::DirRead(reply))) {
274 M2dirCoroutineState::Yielded(M2dirYield::WantsFileExists(probes)) => probes,
275 state => panic!("expected WantsFileExists, got {state:?}"),
276 };
277 let exists: BTreeMap<M2dirPath, bool> = probes.into_iter().map(|p| (p, true)).collect();
278
279 delete.id = "checksum.nonce".into();
280
281 let _ = match delete.resume(Some(M2dirArg::FileExists(exists))) {
282 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(paths)) => paths,
283 state => panic!("expected WantsDirRead, got {state:?}"),
284 };
285
286 let err = match delete.resume(Some(M2dirArg::FileCreate)) {
287 M2dirCoroutineState::Complete(Err(err)) => err,
288 state => panic!("expected Complete(Err), got {state:?}"),
289 };
290 assert!(matches!(err, M2dirEntryDeleteError::UnexpectedArg));
291 }
292}