Skip to main content

io_vdir/
client.rs

1//! Standard, blocking Vdir client driving any coroutine against
2//! [`std::fs`].
3//!
4//! Holds a single filesystem root and exposes one method per common
5//! coroutine. Every method runs its coroutine to completion through
6//! [`VdirClient::run`] by servicing each [`VdirYield`] request via
7//! [`std::fs`].
8
9use alloc::{
10    collections::{BTreeMap, BTreeSet},
11    string::{String, ToString},
12    vec::Vec,
13};
14
15use std::{fs, io};
16
17use getrandom::fill;
18use log::trace;
19use thiserror::Error;
20
21use crate::{
22    collection::{VdirCollection, create::*, delete::*, list::*, rename::*, update::*},
23    coroutine::*,
24    item::{
25        VdirItem, VdirItemKind, copy::*, delete::*, get::*, list::*, locate::*, r#move::*, store::*,
26    },
27    path::VdirPath,
28};
29
30/// Errors returned by the [`VdirClient`] helpers.
31#[derive(Debug, Error)]
32pub enum VdirClientError {
33    /// A collection create coroutine failed.
34    #[error(transparent)]
35    VdirCollectionCreate(#[from] VdirCollectionCreateError),
36    /// A collection delete coroutine failed.
37    #[error(transparent)]
38    VdirCollectionDelete(#[from] VdirCollectionDeleteError),
39    /// A collection list coroutine failed.
40    #[error(transparent)]
41    VdirCollectionList(#[from] VdirCollectionListError),
42    /// A collection rename coroutine failed.
43    #[error(transparent)]
44    VdirCollectionRename(#[from] VdirCollectionRenameError),
45    /// A collection update coroutine failed.
46    #[error(transparent)]
47    VdirCollectionUpdate(#[from] VdirCollectionUpdateError),
48    /// An item locate coroutine failed.
49    #[error(transparent)]
50    VdirItemLocate(#[from] VdirItemLocateError),
51    /// An item get coroutine failed.
52    #[error(transparent)]
53    VdirItemGet(#[from] VdirItemGetError),
54    /// An item list coroutine failed.
55    #[error(transparent)]
56    VdirItemList(#[from] VdirItemListError),
57    /// An item store coroutine failed.
58    #[error(transparent)]
59    VdirItemStore(#[from] VdirItemStoreError),
60    /// An item copy coroutine failed.
61    #[error(transparent)]
62    VdirItemCopy(#[from] VdirItemCopyError),
63    /// An item move coroutine failed.
64    #[error(transparent)]
65    VdirItemMove(#[from] VdirItemMoveError),
66    /// An item delete coroutine failed.
67    #[error(transparent)]
68    VdirItemDelete(#[from] VdirItemDeleteError),
69    /// A filesystem operation failed while servicing a coroutine
70    /// request.
71    #[error(transparent)]
72    Io(#[from] io::Error),
73    /// The system entropy source failed while minting a new item id.
74    #[error("Failed to gather randomness for new item id: {0}")]
75    Random(getrandom::Error),
76}
77
78/// Std-blocking Vdir client wrapping a filesystem root.
79#[derive(Debug)]
80pub struct VdirClient {
81    root: VdirPath,
82}
83
84impl VdirClient {
85    /// Builds a client rooted at `root`. No filesystem check is
86    /// performed at construction time.
87    pub fn new(root: impl Into<VdirPath>) -> Self {
88        Self { root: root.into() }
89    }
90
91    /// Returns the filesystem root this client operates on.
92    pub fn root(&self) -> &VdirPath {
93        &self.root
94    }
95
96    /// Drives any standard-shape coroutine (`Yield = VdirYield`,
97    /// `Return = Result<Output, Error>`) against the local filesystem
98    /// until it terminates.
99    pub fn run<C, T, E>(&self, mut coroutine: C) -> Result<T, VdirClientError>
100    where
101        C: VdirCoroutine<Yield = VdirYield, Return = Result<T, E>>,
102        VdirClientError: From<E>,
103    {
104        let mut arg: Option<VdirReply> = None;
105
106        loop {
107            match coroutine.resume(arg.take()) {
108                VdirCoroutineState::Complete(Ok(out)) => return Ok(out),
109                VdirCoroutineState::Complete(Err(err)) => return Err(err.into()),
110                VdirCoroutineState::Yielded(VdirYield::WantsRandom { len }) => {
111                    let mut bytes = vec![0u8; len];
112                    fill(&mut bytes).map_err(VdirClientError::Random)?;
113                    arg = Some(VdirReply::Random(bytes));
114                }
115                VdirCoroutineState::Yielded(VdirYield::WantsFileExists(paths)) => {
116                    arg = Some(VdirReply::FileExists(file_exists(paths)));
117                }
118                VdirCoroutineState::Yielded(VdirYield::WantsDirExists(paths)) => {
119                    arg = Some(VdirReply::DirExists(dir_exists(paths)));
120                }
121                VdirCoroutineState::Yielded(VdirYield::WantsDirRead(paths)) => {
122                    arg = Some(VdirReply::DirRead(read_dirs(paths)?));
123                }
124                VdirCoroutineState::Yielded(VdirYield::WantsFileRead(paths)) => {
125                    arg = Some(VdirReply::FileRead(read_files(paths)?));
126                }
127                VdirCoroutineState::Yielded(VdirYield::WantsFileCreate(files)) => {
128                    write_files(files)?;
129                    arg = Some(VdirReply::FileCreate);
130                }
131                VdirCoroutineState::Yielded(VdirYield::WantsDirCreate(paths)) => {
132                    create_dirs(paths)?;
133                    arg = Some(VdirReply::DirCreate);
134                }
135                VdirCoroutineState::Yielded(VdirYield::WantsDirRemove(paths)) => {
136                    remove_dirs(paths)?;
137                    arg = Some(VdirReply::DirRemove);
138                }
139                VdirCoroutineState::Yielded(VdirYield::WantsFileRemove(paths)) => {
140                    remove_files(paths)?;
141                    arg = Some(VdirReply::FileRemove);
142                }
143                VdirCoroutineState::Yielded(VdirYield::WantsRename(pairs)) => {
144                    rename_paths(pairs)?;
145                    arg = Some(VdirReply::Rename);
146                }
147                VdirCoroutineState::Yielded(VdirYield::WantsCopy(pairs)) => {
148                    copy_paths(pairs)?;
149                    arg = Some(VdirReply::Copy);
150                }
151            }
152        }
153    }
154
155    /// Runs [`VdirCollectionCreate`]: creates the collection directory and
156    /// writes its metadata files when present.
157    pub fn create_collection(&self, collection: VdirCollection) -> Result<(), VdirClientError> {
158        self.run(VdirCollectionCreate::new(
159            collection,
160            VdirCollectionCreateOptions::default(),
161        ))
162    }
163
164    /// Runs [`VdirCollectionDelete`]: recursively removes the collection
165    /// rooted at `path`.
166    pub fn delete_collection(&self, path: impl Into<VdirPath>) -> Result<(), VdirClientError> {
167        self.run(VdirCollectionDelete::new(
168            path,
169            VdirCollectionDeleteOptions::default(),
170        ))
171    }
172
173    /// Runs [`VdirCollectionList`]: enumerates every collection directly
174    /// under [`self.root`](Self::root).
175    pub fn list_collections(&self) -> Result<BTreeSet<VdirCollection>, VdirClientError> {
176        self.run(VdirCollectionList::new(
177            self.root.clone(),
178            VdirCollectionListOptions::default(),
179        ))
180    }
181
182    /// Runs [`VdirCollectionRename`]: renames the collection at `path` to
183    /// `name` (keeping the same parent directory).
184    pub fn rename_collection(
185        &self,
186        path: impl Into<VdirPath>,
187        name: impl ToString,
188    ) -> Result<(), VdirClientError> {
189        self.run(VdirCollectionRename::new(
190            path,
191            name,
192            VdirCollectionRenameOptions::default(),
193        ))
194    }
195
196    /// Runs [`VdirCollectionUpdate`]: atomically rewrites the metadata of
197    /// `collection`.
198    pub fn update_collection(&self, collection: VdirCollection) -> Result<(), VdirClientError> {
199        self.run(VdirCollectionUpdate::new(
200            collection,
201            VdirCollectionUpdateOptions::default(),
202        ))
203    }
204
205    /// Runs [`VdirItemLocate`]: finds the on-disk path of item `id` inside
206    /// `collection`.
207    pub fn locate_item(
208        &self,
209        collection: impl Into<VdirPath>,
210        id: impl ToString,
211    ) -> Result<(VdirPath, VdirItemKind), VdirClientError> {
212        let VdirItemLocateOutput { path, kind } = self.run(VdirItemLocate::new(
213            collection,
214            id,
215            VdirItemLocateOptions::default(),
216        ))?;
217        Ok((path, kind))
218    }
219
220    /// Runs [`VdirItemGet`]: locates item `id` in `collection` and reads
221    /// its contents from disk.
222    pub fn get_item(
223        &self,
224        collection: impl Into<VdirPath>,
225        id: impl ToString,
226    ) -> Result<VdirItem, VdirClientError> {
227        self.run(VdirItemGet::new(
228            collection,
229            id,
230            VdirItemGetOptions::default(),
231        ))
232    }
233
234    /// Runs [`VdirItemList`]: scans `collection` and returns every
235    /// `.vcf`/`.ics` entry, contents included.
236    pub fn list_items(
237        &self,
238        collection: impl Into<VdirPath>,
239    ) -> Result<BTreeSet<VdirItem>, VdirClientError> {
240        self.run(VdirItemList::new(
241            collection,
242            VdirItemListOptions::default(),
243        ))
244    }
245
246    /// Runs [`VdirItemStore`]: writes `contents` as a new (or updated) item
247    /// under `collection`. Returns the (possibly generated) id and
248    /// final on-disk path.
249    ///
250    /// When `id` is `None`, a fresh UUIDv4 is generated from the system
251    /// entropy source.
252    pub fn store_item(
253        &self,
254        collection: impl Into<VdirPath>,
255        id: Option<String>,
256        kind: VdirItemKind,
257        contents: Vec<u8>,
258    ) -> Result<(String, VdirPath), VdirClientError> {
259        let VdirItemStoreOutput { id, path } = self.run(VdirItemStore::new(
260            collection,
261            id,
262            kind,
263            contents,
264            VdirItemStoreOptions::default(),
265        ))?;
266        Ok((id, path))
267    }
268
269    /// Runs [`VdirItemCopy`]: copies item `id` from `source` into `target`.
270    pub fn copy_item(
271        &self,
272        source: impl Into<VdirPath>,
273        target: impl Into<VdirPath>,
274        id: impl ToString,
275    ) -> Result<(), VdirClientError> {
276        self.run(VdirItemCopy::new(
277            source,
278            target,
279            id,
280            VdirItemCopyOptions::default(),
281        ))
282    }
283
284    /// Runs [`VdirItemMove`]: moves item `id` from `source` into `target`.
285    pub fn move_item(
286        &self,
287        source: impl Into<VdirPath>,
288        target: impl Into<VdirPath>,
289        id: impl ToString,
290    ) -> Result<(), VdirClientError> {
291        self.run(VdirItemMove::new(
292            source,
293            target,
294            id,
295            VdirItemMoveOptions::default(),
296        ))
297    }
298
299    /// Runs [`VdirItemDelete`]: removes item `id` from `collection`.
300    pub fn delete_item(
301        &self,
302        collection: impl Into<VdirPath>,
303        id: impl ToString,
304    ) -> Result<(), VdirClientError> {
305        self.run(VdirItemDelete::new(
306            collection,
307            id,
308            VdirItemDeleteOptions::default(),
309        ))
310    }
311}
312
313fn normalize_path(path: std::path::PathBuf) -> VdirPath {
314    let s = path.to_string_lossy().into_owned();
315    #[cfg(windows)]
316    let s = s.replace('\\', "/");
317    VdirPath::new(s)
318}
319
320fn create_dirs(paths: BTreeSet<VdirPath>) -> Result<(), io::Error> {
321    for path in paths {
322        trace!("create_dir_all {path}");
323        fs::create_dir_all(path.as_str())?;
324    }
325    Ok(())
326}
327
328fn remove_dirs(paths: BTreeSet<VdirPath>) -> Result<(), io::Error> {
329    for path in paths {
330        trace!("remove_dir_all {path}");
331        fs::remove_dir_all(path.as_str())?;
332    }
333    Ok(())
334}
335
336fn remove_files(paths: BTreeSet<VdirPath>) -> Result<(), io::Error> {
337    for path in paths {
338        trace!("remove_file {path}");
339        fs::remove_file(path.as_str())?;
340    }
341    Ok(())
342}
343
344fn write_files(files: BTreeMap<VdirPath, Vec<u8>>) -> Result<(), io::Error> {
345    for (path, contents) in files {
346        trace!("write {path} ({} bytes)", contents.len());
347
348        if let Some(parent) = std::path::Path::new(path.as_str()).parent() {
349            fs::create_dir_all(parent)?;
350        }
351        fs::write(path.as_str(), &contents)?;
352    }
353    Ok(())
354}
355
356fn read_dirs(
357    paths: BTreeSet<VdirPath>,
358) -> Result<BTreeMap<VdirPath, BTreeSet<VdirPath>>, io::Error> {
359    let mut entries = BTreeMap::new();
360
361    for path in paths {
362        trace!("read_dir {path}");
363
364        let mut names = BTreeSet::new();
365        match fs::read_dir(path.as_str()) {
366            Ok(iter) => {
367                for entry in iter {
368                    let entry = entry?;
369                    names.insert(normalize_path(entry.path()));
370                }
371            }
372            Err(err) if err.kind() == io::ErrorKind::NotFound => {}
373            Err(err) => return Err(err),
374        }
375
376        entries.insert(path, names);
377    }
378
379    Ok(entries)
380}
381
382fn read_files(paths: BTreeSet<VdirPath>) -> Result<BTreeMap<VdirPath, Vec<u8>>, io::Error> {
383    let mut contents = BTreeMap::new();
384
385    for path in paths {
386        trace!("read_file {path}");
387        let bytes = fs::read(path.as_str())?;
388        contents.insert(path, bytes);
389    }
390
391    Ok(contents)
392}
393
394fn rename_paths(pairs: Vec<(VdirPath, VdirPath)>) -> Result<(), io::Error> {
395    for (from, to) in pairs {
396        trace!("rename {from} -> {to}");
397        fs::rename(from.as_str(), to.as_str())?;
398    }
399    Ok(())
400}
401
402fn copy_paths(pairs: Vec<(VdirPath, VdirPath)>) -> Result<(), io::Error> {
403    for (from, to) in pairs {
404        trace!("copy {from} -> {to}");
405        fs::copy(from.as_str(), to.as_str())?;
406    }
407    Ok(())
408}
409
410fn file_exists(paths: BTreeSet<VdirPath>) -> BTreeMap<VdirPath, bool> {
411    let mut out = BTreeMap::new();
412    for path in paths {
413        let exists = fs::metadata(path.as_str())
414            .map(|m| m.is_file())
415            .unwrap_or(false);
416        trace!("file_exists {path}: {exists}");
417        out.insert(path, exists);
418    }
419    out
420}
421
422fn dir_exists(paths: BTreeSet<VdirPath>) -> BTreeMap<VdirPath, bool> {
423    let mut out = BTreeMap::new();
424    for path in paths {
425        let exists = fs::metadata(path.as_str())
426            .map(|m| m.is_dir())
427            .unwrap_or(false);
428        trace!("dir_exists {path}: {exists}");
429        out.insert(path, exists);
430    }
431    out
432}