Skip to main content

io_vdir/item/
copy.rs

1//! I/O-free coroutine copying a Vdir item across collections.
2//!
3//! Locates the source item via [`VdirItemLocate`], then copies it into the
4//! target collection keeping the same id and extension.
5//!
6//! The bytes land on the target's `.tmp` sibling and one rename
7//! publishes them, as [`VdirItemStore`] writes its own. Keeping the id
8//! means the target name may already hold an item, and staging is what
9//! keeps that item whole until the copy is complete: it is replaced in
10//! one step or not at all.
11//!
12//! [`VdirItemStore`]: crate::item::store::VdirItemStore
13//!
14//! # Example
15//!
16//! ```rust,no_run
17//! use std::fs;
18//!
19//! use io_vdir::{coroutine::*, item::copy::*};
20//!
21//! let opts = VdirItemCopyOptions::default();
22//! let mut coroutine = VdirItemCopy::new("/tmp/vdir/contacts", "/tmp/vdir/work", "alice", opts);
23//! let mut arg = None;
24//!
25//! loop {
26//!     match coroutine.resume(arg.take()) {
27//!         VdirCoroutineState::Yielded(VdirYield::WantsFileExists(paths)) => {
28//!             let map = paths
29//!                 .into_iter()
30//!                 .map(|p| {
31//!                     let ok = fs::metadata(p.as_str()).map(|m| m.is_file()).unwrap_or(false);
32//!                     (p, ok)
33//!                 })
34//!                 .collect();
35//!             arg = Some(VdirReply::FileExists(map));
36//!         }
37//!         VdirCoroutineState::Yielded(VdirYield::WantsCopy(pairs)) => {
38//!             for (from, to) in pairs {
39//!                 fs::copy(from.as_str(), to.as_str()).unwrap();
40//!             }
41//!             arg = Some(VdirReply::Copy);
42//!         }
43//!         VdirCoroutineState::Yielded(VdirYield::WantsRename(pairs)) => {
44//!             for (from, to) in pairs {
45//!                 fs::rename(from.as_str(), to.as_str()).unwrap();
46//!             }
47//!             arg = Some(VdirReply::Rename);
48//!         }
49//!         VdirCoroutineState::Complete(Ok(())) => break,
50//!         VdirCoroutineState::Complete(Err(err)) => panic!("{err}"),
51//!         state => panic!("unexpected state {state:?}"),
52//!     }
53//! }
54//! ```
55
56use core::{fmt, mem};
57
58use alloc::string::{String, ToString};
59
60use thiserror::Error;
61
62use crate::{
63    coroutine::*,
64    item::{build_paths, locate::*},
65    path::VdirPath,
66    vdir_try,
67};
68
69/// Failure causes during a [`VdirItemCopy`] step.
70#[derive(Clone, Debug, Error)]
71pub enum VdirItemCopyError {
72    /// The driver fed back a reply that does not match the pending
73    /// request.
74    #[error("Vdir item copy failed: unexpected arg {0:?}")]
75    UnexpectedArg(Option<VdirReply>),
76
77    /// The inner locate coroutine failed.
78    #[error(transparent)]
79    Locate(#[from] VdirItemLocateError),
80}
81
82/// Options for [`VdirItemCopy::new`].
83#[derive(Clone, Debug, Default, Eq, PartialEq)]
84pub struct VdirItemCopyOptions {}
85
86/// Copies a Vdir item from a source into a target collection.
87#[derive(Debug)]
88pub struct VdirItemCopy {
89    state: State,
90    #[allow(dead_code)]
91    opts: VdirItemCopyOptions,
92}
93
94impl VdirItemCopy {
95    /// Creates a new coroutine that will copy item `id` from `source`
96    /// into `target`. The item keeps the same id and extension in the
97    /// target collection.
98    pub fn new(
99        source: impl Into<VdirPath>,
100        target: impl Into<VdirPath>,
101        id: impl ToString,
102        opts: VdirItemCopyOptions,
103    ) -> Self {
104        let id = id.to_string();
105        let inner = VdirItemLocate::new(source, &id, VdirItemLocateOptions::default());
106        Self {
107            opts,
108            state: State::Locate {
109                target: target.into(),
110                id,
111                inner,
112            },
113        }
114    }
115}
116
117impl VdirCoroutine for VdirItemCopy {
118    type Yield = VdirYield;
119    type Return = Result<(), VdirItemCopyError>;
120
121    fn resume(&mut self, arg: Option<VdirReply>) -> VdirCoroutineState<Self::Yield, Self::Return> {
122        match (&mut self.state, arg) {
123            (State::Locate { target, id, inner }, arg) => {
124                let out = vdir_try!(inner, arg);
125                let (tmp_path, final_path) = build_paths(target, id, out.kind);
126                let pairs = vec![(out.path, tmp_path.clone())];
127                self.state = State::Copy {
128                    tmp_path,
129                    final_path,
130                };
131                VdirCoroutineState::Yielded(VdirYield::WantsCopy(pairs))
132            }
133            (
134                State::Copy {
135                    tmp_path,
136                    final_path,
137                },
138                Some(VdirReply::Copy),
139            ) => {
140                let pairs = vec![(mem::take(tmp_path), mem::take(final_path))];
141                self.state = State::Rename;
142                VdirCoroutineState::Yielded(VdirYield::WantsRename(pairs))
143            }
144            (State::Rename, Some(VdirReply::Rename)) => VdirCoroutineState::Complete(Ok(())),
145            (_, arg) => {
146                let err = VdirItemCopyError::UnexpectedArg(arg);
147                VdirCoroutineState::Complete(Err(err))
148            }
149        }
150    }
151}
152
153#[derive(Debug)]
154enum State {
155    Locate {
156        target: VdirPath,
157        id: String,
158        inner: VdirItemLocate,
159    },
160    Copy {
161        tmp_path: VdirPath,
162        final_path: VdirPath,
163    },
164    Rename,
165}
166
167impl fmt::Display for State {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        match self {
170            Self::Locate { .. } => f.write_str("locate source item"),
171            Self::Copy { .. } => f.write_str("copy into tmp"),
172            Self::Rename => f.write_str("rename into place"),
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use alloc::collections::BTreeMap;
180
181    use super::*;
182
183    #[test]
184    fn copies_located_source_through_tmp() {
185        let mut cor =
186            VdirItemCopy::new("root/a", "root/b", "alice", VdirItemCopyOptions::default());
187
188        match cor.resume(None) {
189            VdirCoroutineState::Yielded(VdirYield::WantsFileExists(_)) => {}
190            state => panic!("expected WantsFileExists, got {state:?}"),
191        }
192
193        let vcf = VdirPath::from("root/a/alice.vcf");
194        let mut exists = BTreeMap::new();
195        exists.insert(vcf.clone(), true);
196        exists.insert(VdirPath::from("root/a/alice.ics"), false);
197
198        // The bytes land on the tmp sibling, never on the name the
199        // target collection is enumerated by.
200        let staged = VdirPath::from("root/b/alice.vcf.tmp");
201        let pairs = match cor.resume(Some(VdirReply::FileExists(exists))) {
202            VdirCoroutineState::Yielded(VdirYield::WantsCopy(pairs)) => pairs,
203            state => panic!("expected WantsCopy, got {state:?}"),
204        };
205        assert_eq!(pairs, vec![(vcf, staged.clone())]);
206
207        let pairs = match cor.resume(Some(VdirReply::Copy)) {
208            VdirCoroutineState::Yielded(VdirYield::WantsRename(pairs)) => pairs,
209            state => panic!("expected WantsRename, got {state:?}"),
210        };
211        assert_eq!(pairs, vec![(staged, VdirPath::from("root/b/alice.vcf"))]);
212
213        match cor.resume(Some(VdirReply::Rename)) {
214            VdirCoroutineState::Complete(Ok(())) => {}
215            state => panic!("expected Complete(Ok), got {state:?}"),
216        }
217    }
218
219    #[test]
220    fn locate_error_is_forwarded() {
221        let mut cor =
222            VdirItemCopy::new("root/a", "root/b", "alice", VdirItemCopyOptions::default());
223        let _ = cor.resume(None);
224
225        let err = match cor.resume(Some(VdirReply::DirCreate)) {
226            VdirCoroutineState::Complete(Err(err)) => err,
227            state => panic!("expected Complete(Err), got {state:?}"),
228        };
229        assert!(matches!(err, VdirItemCopyError::Locate(_)));
230    }
231}