1use 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#[derive(Debug, Error)]
32pub enum VdirClientError {
33 #[error(transparent)]
35 VdirCollectionCreate(#[from] VdirCollectionCreateError),
36 #[error(transparent)]
38 VdirCollectionDelete(#[from] VdirCollectionDeleteError),
39 #[error(transparent)]
41 VdirCollectionList(#[from] VdirCollectionListError),
42 #[error(transparent)]
44 VdirCollectionRename(#[from] VdirCollectionRenameError),
45 #[error(transparent)]
47 VdirCollectionUpdate(#[from] VdirCollectionUpdateError),
48 #[error(transparent)]
50 VdirItemLocate(#[from] VdirItemLocateError),
51 #[error(transparent)]
53 VdirItemGet(#[from] VdirItemGetError),
54 #[error(transparent)]
56 VdirItemList(#[from] VdirItemListError),
57 #[error(transparent)]
59 VdirItemStore(#[from] VdirItemStoreError),
60 #[error(transparent)]
62 VdirItemCopy(#[from] VdirItemCopyError),
63 #[error(transparent)]
65 VdirItemMove(#[from] VdirItemMoveError),
66 #[error(transparent)]
68 VdirItemDelete(#[from] VdirItemDeleteError),
69 #[error(transparent)]
72 Io(#[from] io::Error),
73 #[error("Failed to gather randomness for new item id: {0}")]
75 Random(getrandom::Error),
76}
77
78#[derive(Debug)]
80pub struct VdirClient {
81 root: VdirPath,
82}
83
84impl VdirClient {
85 pub fn new(root: impl Into<VdirPath>) -> Self {
88 Self { root: root.into() }
89 }
90
91 pub fn root(&self) -> &VdirPath {
93 &self.root
94 }
95
96 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 pub fn create_collection(&self, collection: VdirCollection) -> Result<(), VdirClientError> {
158 self.run(VdirCollectionCreate::new(
159 collection,
160 VdirCollectionCreateOptions::default(),
161 ))
162 }
163
164 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 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 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 pub fn update_collection(&self, collection: VdirCollection) -> Result<(), VdirClientError> {
199 self.run(VdirCollectionUpdate::new(
200 collection,
201 VdirCollectionUpdateOptions::default(),
202 ))
203 }
204
205 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 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 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 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 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 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 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}