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::*,
67 entry::{M2dirEntry, list::*},
68 flag::{M2dirFlags, add::*},
69 m2dir::M2dir,
70 path::M2dirPath,
71};
72
73#[derive(Clone, Debug, Error)]
75pub enum M2dirEntryDeleteError {
76 #[error("M2DIR DELETE failed: unexpected coroutine arg")]
78 UnexpectedArg,
79 #[error("M2DIR DELETE failed: missing coroutine arg")]
81 MissingArg,
82 #[error("M2DIR DELETE failed: entry {0} not found")]
84 NotFound(String),
85 #[error("M2DIR DELETE failed: {0}")]
87 List(#[from] M2dirEntryListError),
88 #[error("M2DIR DELETE failed: {0}")]
90 AddFlags(#[from] M2dirFlagAddError),
91}
92
93#[derive(Clone, Debug, Default, Eq, PartialEq)]
95pub struct M2dirEntryDeleteOptions {
96 pub soft_delete: Option<M2dirFlags>,
102}
103
104pub struct M2dirEntryDelete {
106 id: String,
107 m2dir: M2dir,
108 state: State,
109 opts: M2dirEntryDeleteOptions,
110}
111
112impl M2dirEntryDelete {
113 pub fn new(m2dir: M2dir, id: impl ToString, opts: M2dirEntryDeleteOptions) -> Self {
117 Self {
118 id: id.to_string(),
119 state: State::List(M2dirEntryList::new(
120 m2dir.clone(),
121 M2dirEntryListOptions::default(),
122 )),
123 m2dir,
124 opts,
125 }
126 }
127}
128
129impl M2dirCoroutine for M2dirEntryDelete {
130 type Yield = M2dirYield;
131 type Return = Result<(), M2dirEntryDeleteError>;
132
133 fn resume(&mut self, arg: Option<M2dirArg>) -> M2dirCoroutineState<Self::Yield, Self::Return> {
134 match (&mut self.state, arg) {
135 (State::List(list), arg) => match list.resume(arg) {
136 M2dirCoroutineState::Yielded(yld) => M2dirCoroutineState::Yielded(yld),
137 M2dirCoroutineState::Complete(Ok(entries)) => {
138 let Some(entry) = entries.into_iter().find(|e| e.id() == self.id) else {
139 let err = M2dirEntryDeleteError::NotFound(self.id.clone());
140 return M2dirCoroutineState::Complete(Err(err));
141 };
142
143 trace!("located entry at {}", entry.path());
144
145 if let Some(flags) = self.opts.soft_delete.clone() {
146 trace!("soft-deleting entry: adding flags instead of removing");
147 let mut add = M2dirFlagAdd::new(
148 &self.m2dir,
149 &self.id,
150 flags,
151 M2dirFlagAddOptions::default(),
152 );
153 let first = add.resume(None);
154 self.state = State::SoftDeleting(add);
155 return match first {
156 M2dirCoroutineState::Yielded(yld) => M2dirCoroutineState::Yielded(yld),
157 M2dirCoroutineState::Complete(Ok(())) => {
158 M2dirCoroutineState::Complete(Ok(()))
159 }
160 M2dirCoroutineState::Complete(Err(err)) => {
161 M2dirCoroutineState::Complete(Err(err.into()))
162 }
163 };
164 }
165
166 let paths = BTreeSet::from_iter([self.m2dir.meta_dir()]);
167 self.state = State::ReadMeta(entry);
168 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(paths))
169 }
170 M2dirCoroutineState::Complete(Err(err)) => {
171 M2dirCoroutineState::Complete(Err(err.into()))
172 }
173 },
174 (State::ReadMeta(entry), Some(M2dirArg::DirRead(entries))) => {
175 let meta_names = entries.into_values().next().unwrap_or_default();
176 let entry = mem::replace(
177 entry,
178 M2dirEntry::from_parts(String::new(), M2dirPath::default()),
179 );
180
181 let mut to_remove: Vec<M2dirPath> = Vec::new();
182 to_remove.push(entry.path().clone());
183
184 for path in meta_names {
185 let Some(name) = path.file_name() else {
186 continue;
187 };
188 if name.starts_with(&self.id) {
189 to_remove.push(path);
190 }
191 }
192
193 trace!("wants removal of {} files", to_remove.len());
194 self.state = State::Removing;
195 M2dirCoroutineState::Yielded(M2dirYield::WantsFileRemove(BTreeSet::from_iter(
196 to_remove,
197 )))
198 }
199 (State::Removing, Some(M2dirArg::FileRemove)) => {
200 trace!("entry deleted");
201 M2dirCoroutineState::Complete(Ok(()))
202 }
203 (State::SoftDeleting(add), arg) => match add.resume(arg) {
204 M2dirCoroutineState::Yielded(yld) => M2dirCoroutineState::Yielded(yld),
205 M2dirCoroutineState::Complete(Ok(())) => {
206 trace!("entry soft-deleted");
207 M2dirCoroutineState::Complete(Ok(()))
208 }
209 M2dirCoroutineState::Complete(Err(err)) => {
210 M2dirCoroutineState::Complete(Err(err.into()))
211 }
212 },
213 (_, Some(_)) => {
214 let err = M2dirEntryDeleteError::UnexpectedArg;
215 M2dirCoroutineState::Complete(Err(err))
216 }
217 (_, None) => {
218 let err = M2dirEntryDeleteError::MissingArg;
219 M2dirCoroutineState::Complete(Err(err))
220 }
221 }
222 }
223}
224
225enum State {
226 List(M2dirEntryList),
227 ReadMeta(M2dirEntry),
228 Removing,
229 SoftDeleting(M2dirFlagAdd),
230}
231
232impl fmt::Display for State {
233 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234 match self {
235 Self::List(_) => f.write_str("locate entry"),
236 Self::ReadMeta(_) => f.write_str("read .meta directory"),
237 Self::Removing => f.write_str("removing files"),
238 Self::SoftDeleting(_) => f.write_str("soft-deleting (adding flags)"),
239 }
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use alloc::collections::BTreeMap;
246
247 use super::*;
248
249 #[test]
250 fn missing_entry_returns_not_found_error() {
251 let m2dir = M2dir::from_path("/tmp/inbox");
252 let mut delete =
253 M2dirEntryDelete::new(m2dir, "missing", M2dirEntryDeleteOptions::default());
254
255 let probes = match delete.resume(None) {
256 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(paths)) => paths,
257 state => panic!("expected WantsDirRead, got {state:?}"),
258 };
259 let dir = probes.into_iter().next().unwrap();
260 let mut reply = BTreeMap::new();
261 reply.insert(dir, BTreeSet::new());
262
263 let err = match delete.resume(Some(M2dirArg::DirRead(reply))) {
264 M2dirCoroutineState::Complete(Err(err)) => err,
265 state => panic!("expected Complete(Err), got {state:?}"),
266 };
267 assert!(matches!(err, M2dirEntryDeleteError::NotFound(id) if id == "missing"));
268 }
269
270 #[test]
271 fn list_error_propagates_via_from() {
272 let m2dir = M2dir::from_path("/tmp/inbox");
273 let mut delete = M2dirEntryDelete::new(m2dir, "x", M2dirEntryDeleteOptions::default());
274
275 let _ = delete.resume(None);
276 let err = match delete.resume(Some(M2dirArg::FileCreate)) {
277 M2dirCoroutineState::Complete(Err(err)) => err,
278 state => panic!("expected Complete(Err), got {state:?}"),
279 };
280 assert!(matches!(err, M2dirEntryDeleteError::List(_)));
281 }
282
283 #[test]
284 fn unexpected_arg_at_list_initial_returns_list_error() {
285 let m2dir = M2dir::from_path("/tmp/inbox");
286 let mut delete = M2dirEntryDelete::new(m2dir, "x", M2dirEntryDeleteOptions::default());
287
288 let err = match delete.resume(Some(M2dirArg::FileRead(BTreeMap::new()))) {
289 M2dirCoroutineState::Complete(Err(err)) => err,
290 state => panic!("expected Complete(Err), got {state:?}"),
291 };
292 assert!(matches!(err, M2dirEntryDeleteError::List(_)));
293 }
294
295 #[test]
296 fn missing_arg_at_list_propagates_via_list_error() {
297 let m2dir = M2dir::from_path("/tmp/inbox");
298 let mut delete = M2dirEntryDelete::new(m2dir, "x", M2dirEntryDeleteOptions::default());
299 let _ = delete.resume(None);
300
301 let err = match delete.resume(None) {
302 M2dirCoroutineState::Complete(Err(err)) => err,
303 state => panic!("expected Complete(Err), got {state:?}"),
304 };
305 assert!(matches!(err, M2dirEntryDeleteError::List(_)));
306 }
307
308 #[test]
309 fn read_meta_unexpected_arg_returns_unexpected_arg_error() {
310 let m2dir = M2dir::from_path("/tmp/inbox");
311 let mut delete = M2dirEntryDelete::new(m2dir, "any", M2dirEntryDeleteOptions::default());
312
313 let probes = match delete.resume(None) {
314 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(paths)) => paths,
315 state => panic!("expected WantsDirRead, got {state:?}"),
316 };
317 let dir = probes.into_iter().next().unwrap();
318 let mut names = BTreeSet::new();
319 let entry_path = M2dirPath::from("/tmp/inbox/123,checksum.nonce");
320 names.insert(entry_path.clone());
321 let mut reply = BTreeMap::new();
322 reply.insert(dir, names);
323
324 let probes = match delete.resume(Some(M2dirArg::DirRead(reply))) {
325 M2dirCoroutineState::Yielded(M2dirYield::WantsFileExists(probes)) => probes,
326 state => panic!("expected WantsFileExists, got {state:?}"),
327 };
328 let exists: BTreeMap<M2dirPath, bool> = probes.into_iter().map(|p| (p, true)).collect();
329
330 delete.id = "checksum.nonce".into();
331
332 let _ = match delete.resume(Some(M2dirArg::FileExists(exists))) {
333 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(paths)) => paths,
334 state => panic!("expected WantsDirRead, got {state:?}"),
335 };
336
337 let err = match delete.resume(Some(M2dirArg::FileCreate)) {
338 M2dirCoroutineState::Complete(Err(err)) => err,
339 state => panic!("expected Complete(Err), got {state:?}"),
340 };
341 assert!(matches!(err, M2dirEntryDeleteError::UnexpectedArg));
342 }
343
344 #[test]
345 fn soft_delete_adds_flags_instead_of_removing() {
346 let m2dir = M2dir::from_path("/tmp/inbox");
347 let mut flags = M2dirFlags::default();
348 flags.insert("\\Deleted");
349 let opts = M2dirEntryDeleteOptions {
350 soft_delete: Some(flags),
351 };
352 let mut delete = M2dirEntryDelete::new(m2dir, "checksum.nonce", opts);
353
354 let probes = match delete.resume(None) {
356 M2dirCoroutineState::Yielded(M2dirYield::WantsDirRead(paths)) => paths,
357 state => panic!("expected WantsDirRead, got {state:?}"),
358 };
359 let dir = probes.into_iter().next().unwrap();
360 let mut names = BTreeSet::new();
361 names.insert(M2dirPath::from("/tmp/inbox/123,checksum.nonce"));
362 let mut reply = BTreeMap::new();
363 reply.insert(dir, names);
364
365 let probes = match delete.resume(Some(M2dirArg::DirRead(reply))) {
366 M2dirCoroutineState::Yielded(M2dirYield::WantsFileExists(probes)) => probes,
367 state => panic!("expected WantsFileExists, got {state:?}"),
368 };
369 let exists: BTreeMap<M2dirPath, bool> = probes.into_iter().map(|p| (p, true)).collect();
370
371 let read = match delete.resume(Some(M2dirArg::FileExists(exists))) {
373 M2dirCoroutineState::Yielded(M2dirYield::WantsFileRead(paths)) => paths,
374 state => panic!("expected WantsFileRead, got {state:?}"),
375 };
376 let flags_path = read.into_iter().next().unwrap();
377 let reply = BTreeMap::from_iter([(flags_path, alloc::vec::Vec::<u8>::new())]);
378
379 let files = match delete.resume(Some(M2dirArg::FileRead(reply))) {
380 M2dirCoroutineState::Yielded(M2dirYield::WantsFileCreate(files)) => files,
381 state => panic!("expected WantsFileCreate, got {state:?}"),
382 };
383 let (_, bytes) = files.into_iter().next().unwrap();
384 let serialized = alloc::string::String::from_utf8(bytes).unwrap();
385 assert!(serialized.contains("\\Deleted"));
386
387 match delete.resume(Some(M2dirArg::FileCreate)) {
388 M2dirCoroutineState::Complete(Ok(())) => {}
389 state => panic!("expected Complete(Ok), got {state:?}"),
390 }
391 }
392}