1//! Notebooks a document provider keeps, as Android's Storage Access Framework reaches them:
2//! documents named by opaque ids, listed by folder and opened as descriptors, with no locks,
3//! no rename over another document and, on many providers, no seeking. A picked folder is a
4//! notebook root (`session::Notebook::open_provider`), a picked document a lone section
5//! (`session::Section::open_provider`); replicas and the catalog's listing stay in the cache.
6
7use crate::{Error, Result, discover, session::Storage};
8use onestore::{CommitError, CommitIo, CommitState, Stamp, Store, Transaction};
9use std::{
10 collections::BTreeMap,
11 fs::File,
12 io::{self, Read, Seek, SeekFrom, Write},
13 path::PathBuf,
14 sync::{Arc, Mutex, MutexGuard, PoisonError},
15 time::Duration,
16};
17
18/// A document as its provider describes it.
19#[derive(Clone, Debug)]
20pub struct Document {
21 pub id: String,
22 pub name: String,
23 pub directory: bool,
24 /// What the provider lists, where it lists anything.
25 pub size: Option<u64>,
26 pub modified: Option<u64>,
27 /// The grant allows writing and the provider supports it.
28 pub writable: bool,
29}
30
31/// The host's document provider. Failures take the shapes a provider gives them: unreachable
32/// as `NotConnected`, a revoked grant as `PermissionDenied`, and a document gone (or renamed,
33/// which changes its id) as `NotFound`.
34pub trait Provider: Send + Sync {
35 fn document(&self, id: &str) -> io::Result<Document>;
36 fn children(&self, id: &str) -> io::Result<Vec<Document>>;
37 /// Opens a document in a `ContentResolver` mode: `r`, `rw`, `wt` or `w`.
38 fn open(&self, id: &str, mode: &str) -> io::Result<File>;
39 /// Closes a file `open` gave, with the provider's verdict on what was written to it.
40 fn close(&self, file: File) -> io::Result<()>;
41 /// Renames a document, returning its new id.
42 fn rename(&self, id: &str, name: &str) -> io::Result<String>;
43}
44
45/// The documents under one root a provider keeps: a notebook's folder or a lone section.
46/// Paths are catalog paths, `/`-separated below the root.
47pub struct Documents {
48 provider: Box<dyn Provider>,
49 pub(crate) root: String,
50 /// The app's cache, holding the replicas and the notebook's listing.
51 pub(crate) cache: PathBuf,
52 /// This device as the conflict pages its merged-back publications make name it.
53 device: String,
54 /// Each path's document as last listed; a stale one is listed again.
55 ids: Mutex<BTreeMap<String, String>>,
56}
57
58impl Documents {
59 pub fn new(
60 provider: impl Provider + 'static,
61 root: impl Into<String>,
62 cache: impl Into<PathBuf>,
63 device: impl Into<String>,
64 ) -> Self {
65 Self {
66 provider: Box::new(provider),
67 root: root.into(),
68 cache: cache.into(),
69 device: device.into(),
70 ids: Mutex::default(),
71 }
72 }
73
74 pub(crate) fn location(&self) -> String {
75 format!("provider:{}", self.root)
76 }
77
78 /// Where this device keeps the images it published until the documents are seen holding
79 /// them: a provider without compare-and-swap may let another device's upload replace one.
80 fn published(&self) -> PathBuf {
81 crate::location::folder(&self.cache, &self.location()).join("published")
82 }
83
84 fn id(&self, path: &str) -> io::Result<String> {
85 if path.is_empty() {
86 return Ok(self.root.clone());
87 }
88 if let Some(id) = lock(&self.ids).get(path) {
89 return Ok(id.clone());
90 }
91 self.listing(path.rsplit_once('/').map_or("", |(folder, _)| folder))?;
92 lock(&self.ids)
93 .get(path)
94 .cloned()
95 .ok_or_else(|| io::ErrorKind::NotFound.into())
96 }
97
98 /// Reads through the document at `path`, listing its folders again once if the id they
99 /// last gave is gone.
100 fn with_id<T>(&self, path: &str, read: impl Fn(&str) -> io::Result<T>) -> io::Result<T> {
101 match read(&self.id(path)?) {
102 Err(error) if error.kind() == io::ErrorKind::NotFound && !path.is_empty() => {
103 lock(&self.ids).clear();
104 read(&self.id(path)?)
105 }
106 result => result,
107 }
108 }
109
110 /// A folder's entries, and the documents sharing a name with one listed before them by id.
111 fn listing(&self, folder: &str) -> io::Result<(Vec<discover::Entry>, Vec<Document>)> {
112 let mut children = self.with_id(folder, |id| self.provider.children(id))?;
113 children.sort_by(|a, b| (&a.name, &a.id).cmp(&(&b.name, &b.id)));
114 let (mut entries, mut duplicates) = (Vec::<discover::Entry>::new(), Vec::new());
115 let mut ids = lock(&self.ids);
116 for child in children {
117 if entries.last().is_some_and(|last| last.name == child.name) {
118 duplicates.push(child);
119 continue;
120 }
121 ids.insert(discover::join(folder, &child.name), child.id.clone());
122 entries.push(discover::Entry {
123 name: child.name,
124 kind: if child.directory {
125 discover::EntryKind::Directory
126 } else {
127 discover::EntryKind::File
128 },
129 listed: discover::Listed {
130 size: child.size.unwrap_or_default(),
131 // Unknown, it never matches an earlier listing, so the file is read again.
132 modified: child
133 .modified
134 .filter(|_| child.size.is_some())
135 .unwrap_or_else(crate::now),
136 },
137 });
138 }
139 Ok((entries, duplicates))
140 }
141
142 pub(crate) fn entries(&self, folder: &str) -> io::Result<Vec<discover::Entry>> {
143 Ok(self.listing(folder)?.0)
144 }
145
146 /// Opens the document `id`, hands it to `operation` and closes it, failing if either fails.
147 fn opened<T>(
148 &self,
149 id: &str,
150 mode: &str,
151 operation: impl FnOnce(&mut File) -> io::Result<T>,
152 ) -> io::Result<T> {
153 let mut file = self.provider.open(id, mode)?;
154 let result = operation(&mut file);
155 let closed = self.provider.close(file);
156 let value = result?;
157 closed?;
158 Ok(value)
159 }
160
161 fn read(&self, path: &str, limit: usize) -> io::Result<Vec<u8>> {
162 self.with_id(path, |id| self.opened(id, "r", |file| whole(file, limit)))
163 }
164
165 /// The document's stamp, with its image where a stream had to be read whole for it.
166 fn stamp(&self, path: &str) -> io::Result<(Stamp, Option<Vec<u8>>)> {
167 self.with_id(path, |id| {
168 self.opened(id, "r", |file| {
169 if file.stream_position().is_err() {
170 let image = whole(file, crate::MAX_FILE_BYTES)?;
171 // A torn image shorter than a header still stamps, as it stands.
172 let mut header = [0; 1024];
173 let start = &image[..image.len().min(1024)];
174 header[..start.len()].copy_from_slice(start);
175 let length = image.len() as u64;
176 return Ok((Stamp { header, length }, Some(image)));
177 }
178 let mut header = [0; 1024];
179 file.read_exact(&mut header)?;
180 let length = file.seek(SeekFrom::End(0))?;
181 Ok((Stamp { header, length }, None))
182 })
183 })
184 }
185
186 /// Writes the document at `path` through `operation`, provided its grant allows writing:
187 /// in place where its `r` descriptor seeks, else on the image read whole, which then
188 /// replaces it. `keep` takes what was written: the descriptor, once the commit landed, or
189 /// the image, once a write to the document opened. The id is the one the last listing gave,
190 /// never listed again, so that a document taking the name meanwhile is not the one written.
191 fn write(
192 &self,
193 path: &str,
194 operation: impl FnOnce(&mut Io) -> std::result::Result<(), CommitError>,
195 keep: impl FnOnce(Written<'_>) -> io::Result<()>,
196 ) -> std::result::Result<(), CommitError> {
197 let id = self.id(path).map_err(refused)?;
198 self.writable(&id).map_err(refused)?;
199 let seekable = self
200 .opened(&id, "r", |file| Ok(file.stream_position().is_ok()))
201 .map_err(refused)?;
202 let mut io = if seekable {
203 Io::File(self.provider.open(&id, "rw").map_err(refused)?)
204 } else {
205 let image = self.opened(&id, "r", |file| whole(file, crate::MAX_FILE_BYTES));
206 Io::Image(image.map_err(refused)?)
207 };
208 let result = operation(&mut io);
209 match io {
210 Io::File(mut file) => {
211 // A copy it could not keep only leaves the publication unprotected.
212 if result.is_ok() {
213 let _ = keep(Written::InPlace(&mut file));
214 }
215 let closed = self.provider.close(file);
216 result?;
217 closed.map_err(uncertain)
218 }
219 Io::Image(image) => {
220 result?;
221 self.upload(&id, &image, keep)
222 }
223 }
224 }
225
226 /// Fails unless the grant allows writing the document `id`.
227 fn writable(&self, id: &str) -> io::Result<()> {
228 if self.provider.document(id)?.writable {
229 return Ok(());
230 }
231 Err(io::Error::new(
232 io::ErrorKind::ReadOnlyFilesystem,
233 "The app keeping this section lets Snowbound only read it",
234 ))
235 }
236
237 /// Writes `image` whole as the document `id`, with `wt`, or `w` where that is refused
238 /// (images only grow), having `keep` take it once a write opened.
239 fn upload(
240 &self,
241 id: &str,
242 image: &[u8],
243 keep: impl FnOnce(Written<'_>) -> io::Result<()>,
244 ) -> std::result::Result<(), CommitError> {
245 // A provider refusing `wt` may say so as any failure but a revoked grant.
246 let mut file = match self.provider.open(id, "wt") {
247 Err(error) if error.kind() != io::ErrorKind::PermissionDenied => {
248 self.provider.open(id, "w")
249 }
250 opened => opened,
251 }
252 .map_err(refused)?;
253 if let Err(error) = keep(Written::Image(image)) {
254 let _ = self.provider.close(file);
255 return Err(refused(error));
256 }
257 let written = file.write_all(image);
258 let closed = self.provider.close(file);
259 written.and(closed).map_err(uncertain)
260 }
261
262 /// Renames the document at `path` within its folder.
263 fn rename(&self, path: &str, name: &str) -> io::Result<()> {
264 self.provider.rename(&self.id(path)?, name)?;
265 lock(&self.ids).clear();
266 Ok(())
267 }
268}
269
270fn refused(error: io::Error) -> CommitError {
271 CommitError {
272 state: CommitState::NotCommitted,
273 error,
274 }
275}
276
277fn uncertain(error: io::Error) -> CommitError {
278 CommitError {
279 state: CommitState::Unknown,
280 error,
281 }
282}
283
284fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
285 mutex.lock().unwrap_or_else(PoisonError::into_inner)
286}
287
288fn whole(file: &mut File, limit: usize) -> io::Result<Vec<u8>> {
289 let mut bytes = Vec::new();
290 file.take(limit as u64 + 1).read_to_end(&mut bytes)?;
291 if bytes.len() > limit {
292 return Err(io::ErrorKind::FileTooLarge.into());
293 }
294 Ok(bytes)
295}
296
297/// What a write wrote: the document in place, or its whole image.
298enum Written<'a> {
299 InPlace(&'a mut File),
300 Image(&'a [u8]),
301}
302
303/// A document as a commit writes it: its seekable descriptor, or its image to write whole.
304enum Io {
305 File(File),
306 Image(Vec<u8>),
307}
308
309impl CommitIo for Io {
310 fn read_at(&mut self, offset: u64, bytes: &mut [u8]) -> io::Result<usize> {
311 match self {
312 Io::File(file) => {
313 file.seek(SeekFrom::Start(offset))?;
314 file.read(bytes)
315 }
316 Io::Image(image) => {
317 let rest = usize::try_from(offset)
318 .ok()
319 .and_then(|offset| image.get(offset..))
320 .unwrap_or_default();
321 let count = rest.len().min(bytes.len());
322 bytes[..count].copy_from_slice(&rest[..count]);
323 Ok(count)
324 }
325 }
326 }
327
328 fn write_at(&mut self, offset: u64, bytes: &[u8]) -> io::Result<usize> {
329 match self {
330 Io::File(file) => {
331 file.seek(SeekFrom::Start(offset))?;
332 file.write(bytes)
333 }
334 Io::Image(image) => {
335 let offset = usize::try_from(offset).map_err(|_| io::ErrorKind::FileTooLarge)?;
336 let end = offset + bytes.len();
337 if image.len() < end {
338 image.resize(end, 0);
339 }
340 image[offset..end].copy_from_slice(bytes);
341 Ok(bytes.len())
342 }
343 }
344 }
345
346 fn flush(&mut self) -> io::Result<()> {
347 match self {
348 Io::File(file) => file.sync_all(),
349 Io::Image(_) => Ok(()),
350 }
351 }
352}
353
354/// The hex of a stamp's digest, which names an image this device keeps.
355fn digest(stamp: &Stamp) -> String {
356 use sha2::Digest;
357 let digest = sha2::Sha256::new()
358 .chain_update(stamp.header)
359 .chain_update(stamp.length.to_le_bytes())
360 .finalize();
361 hex(&digest[..16])
362}
363
364fn hex(bytes: &[u8]) -> String {
365 bytes.iter().map(|byte| format!("{byte:02x}")).collect()
366}
367
368fn file_of(header: &[u8]) -> Option<[u8; 16]> {
369 Some(onestore::Header::parse(header).ok()?.file_id)
370}
371
372/// An image this device keeps, as its name gives it: `<document>-<image>-<base>.one`, the
373/// document's identity, then the digests of the image's stamp and of the stamp of the image
374/// it was published on.
375struct Kept {
376 name: String,
377 document: String,
378 image: String,
379 base: String,
380}
381
382impl Kept {
383 fn named(name: String) -> Option<Self> {
384 let mut parts = name.strip_suffix(".one")?.split('-').map(str::to_owned);
385 let (document, image, base) = (parts.next()?, parts.next()?, parts.next()?);
386 Some(Self {
387 name,
388 document,
389 image,
390 base,
391 })
392 }
393}
394
395/// How long reads may lag this device's own upload before a document still standing as the
396/// image the upload was made on counts as having lost it.
397const LAG: Duration = Duration::from_secs(60);
398
399/// A section a document provider keeps, as its replica publishes to it. Each image it
400/// publishes is kept, and listed as a version once the document is seen otherwise, until the
401/// document holds it: an upload another device's replaced then merges back as a conflict
402/// version does (`resolve.rs`). Only this device's publications are kept so.
403pub(crate) struct ProviderRemote {
404 documents: Arc<Documents>,
405 path: String,
406 /// The image the step read whole for a stamp, which its read takes.
407 read: Option<Vec<u8>>,
408}
409
410impl ProviderRemote {
411 pub(crate) fn new(documents: &Arc<Documents>, path: &str) -> Self {
412 Self {
413 documents: Arc::clone(documents),
414 path: path.to_owned(),
415 read: None,
416 }
417 }
418
419 fn kept(&self) -> Vec<(Kept, std::time::SystemTime)> {
420 let entries = crate::fs::read_dir(self.documents.published())
421 .into_iter()
422 .flatten();
423 entries
424 .filter_map(|entry| {
425 let entry = entry.ok()?;
426 let kept = Kept::named(entry.file_name().into_string().ok()?)?;
427 Some((kept, entry.metadata().ok()?.modified().ok()?))
428 })
429 .collect()
430 }
431
432 /// Keeps the image `transaction` published, in place of the one it was published on:
433 /// that kept image with the transaction applied where this device kept it, else read back.
434 fn keep(&self, written: Written<'_>, transaction: &Transaction) -> io::Result<()> {
435 let folder = self.documents.published();
436 let base = digest(transaction.base());
437 let document = file_of(&transaction.base().header).ok_or(io::ErrorKind::InvalidData)?;
438 let on = (self.kept().into_iter())
439 .find(|(kept, _)| kept.document == hex(&document) && kept.image == base);
440 let image = match (written, &on) {
441 (Written::Image(image), _) => image.to_vec(),
442 (Written::InPlace(_), Some((kept, _))) => {
443 let mut image = crate::fs::read(folder.join(&kept.name))?;
444 transaction
445 .apply(&mut image)
446 .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
447 image
448 }
449 (Written::InPlace(file), None) => {
450 file.seek(SeekFrom::Start(0))?;
451 whole(file, crate::MAX_FILE_BYTES)?
452 }
453 };
454 let name = format!(
455 "{}-{}-{base}.one",
456 hex(&document),
457 digest(&transaction.stamp())
458 );
459 crate::fs::create_dir_all(&folder)?;
460 let mut kept = tempfile::NamedTempFile::new_in(&folder)?;
461 kept.write_all(&image)?;
462 kept.as_file().sync_all()?;
463 kept.persist(folder.join(name))
464 .map_err(|error| error.error)?;
465 if let Some((kept, _)) = on {
466 crate::fs::remove_file(folder.join(kept.name))?;
467 }
468 Ok(())
469 }
470
471 /// This device's own upload cut off, as a provider that writes non-atomically leaves it,
472 /// put back and read again: the newest image kept that the torn bytes begin. Any other
473 /// document that does not parse is left as it is.
474 fn repair(&self, torn: &[u8]) -> io::Result<Vec<u8>> {
475 let mut newest = None;
476 for (kept, at) in self.kept() {
477 let image = crate::fs::read(self.documents.published().join(&kept.name))?;
478 if image.len() > torn.len() && image.starts_with(torn) {
479 newest = newest.max(Some((at, image)));
480 }
481 }
482 let (_, image) = newest.ok_or(io::ErrorKind::WouldBlock)?;
483 let id = self.documents.id(&self.path)?;
484 self.documents.writable(&id)?;
485 self.documents
486 .upload(&id, &image, |_| Ok(()))
487 .map_err(|error| error.error)?;
488 self.documents.read(&self.path, crate::MAX_FILE_BYTES)
489 }
490}
491
492fn parses(image: &[u8]) -> bool {
493 Store::parse(image).is_ok_and(|store| store.checksum_mismatches.is_empty())
494}
495
496impl crate::Remote for ProviderRemote {
497 /// A torn or damaged image, as a stream mid-upload reads, is never a base to rebase on.
498 fn read(&mut self) -> io::Result<Vec<u8>> {
499 let image = match self.read.take() {
500 Some(image) => image,
501 None => self.documents.read(&self.path, crate::MAX_FILE_BYTES)?,
502 };
503 if !parses(&image) {
504 return Err(io::Error::new(
505 io::ErrorKind::WouldBlock,
506 "The section was read mid-write",
507 ));
508 }
509 Ok(image)
510 }
511
512 fn stamp(&mut self) -> io::Result<Stamp> {
513 if let Some(image) = &self.read {
514 return Stamp::of(image)
515 .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error));
516 }
517 let (stamp, image) = self.documents.stamp(&self.path)?;
518 self.read = image;
519 Ok(stamp)
520 }
521
522 fn publish(&mut self, transaction: &Transaction) -> std::result::Result<(), CommitError> {
523 self.read = None;
524 self.documents.write(
525 &self.path,
526 |io| transaction.commit(io),
527 |written| self.keep(written, transaction),
528 )
529 }
530
531 fn confirm(&mut self, base: &Stamp) -> std::result::Result<(), CommitError> {
532 self.read = None;
533 self.documents
534 .write(&self.path, |io| onestore::confirm(io, base), |_| Ok(()))
535 }
536
537 /// The images kept of this document other than the one it stands as; one whose upload
538 /// the document still stands before is listed only once reads have had `LAG` to catch up.
539 /// A document read torn is repaired first, once a step.
540 fn versions(&mut self) -> io::Result<Vec<crate::Version>> {
541 let (mut stamp, mut image) = self.documents.stamp(&self.path)?;
542 if let Some(torn) = image.as_ref().filter(|image| !parses(image))
543 && let Ok(repaired) = self.repair(torn)
544 && let Ok(repaired_stamp) = Stamp::of(&repaired)
545 {
546 (stamp, image) = (repaired_stamp, Some(repaired));
547 }
548 self.read = image;
549 // A header mid-write names no document; the step's read repairs it.
550 let Some(document) = file_of(&stamp.header) else {
551 return Ok(Vec::new());
552 };
553 let (document, current) = (hex(&document), digest(&stamp));
554 let lagging = |kept: &Kept, at: std::time::SystemTime| {
555 kept.base == current && at.elapsed().is_ok_and(|elapsed| elapsed < LAG)
556 };
557 Ok((self.kept().into_iter())
558 .filter(|(kept, at)| {
559 kept.document == document && kept.image != current && !lagging(kept, *at)
560 })
561 .map(|(kept, _)| crate::Version {
562 id: kept.name,
563 device: Some(self.documents.device.clone()),
564 })
565 .collect())
566 }
567
568 fn version(&mut self, id: &str) -> io::Result<Vec<u8>> {
569 crate::fs::read(self.documents.published().join(id))
570 }
571
572 /// One that cannot merge, of another section under the document's name, is set aside
573 /// whole, where `versions` lists it no more: no provider here takes a new document beside it.
574 fn retire(&mut self, id: &str, keep: bool) -> io::Result<()> {
575 let path = self.documents.published().join(id);
576 if keep {
577 crate::fs::rename(&path, path.with_extension("unmerged"))
578 } else {
579 crate::fs::remove_file(path)
580 }
581 }
582}
583
584/// A notebook folder a document provider keeps, its folders as last listed kept at `listed`.
585pub(crate) struct Tree {
586 pub(crate) documents: Arc<Documents>,
587 pub(crate) listed: PathBuf,
588}
589
590/// The provider's folders, or as they were last listed while it can't be reached.
591struct Source<'a> {
592 documents: &'a Documents,
593 kept: BTreeMap<String, Vec<discover::Entry>>,
594 listed: BTreeMap<String, Vec<discover::Entry>>,
595 /// Documents sharing a name with one listed, by folder.
596 duplicates: BTreeMap<String, Vec<Document>>,
597}
598
599impl discover::Source for Source<'_> {
600 fn entries(&mut self, path: &str, limit: usize) -> io::Result<Vec<discover::Entry>> {
601 let entries = match self.documents.listing(path) {
602 Ok((entries, duplicates)) => {
603 self.duplicates.insert(path.to_owned(), duplicates);
604 entries
605 }
606 Err(error) if error.kind() == io::ErrorKind::NotConnected => {
607 self.kept.get(path).cloned().ok_or(error)?
608 }
609 Err(error) => return Err(error),
610 };
611 if entries.len() > limit {
612 return Err(io::ErrorKind::FileTooLarge.into());
613 }
614 self.listed.insert(path.to_owned(), entries.clone());
615 Ok(entries)
616 }
617
618 fn read(&mut self, path: &str, limit: usize) -> io::Result<Vec<u8>> {
619 self.documents.read(path, limit)
620 }
621}
622
623fn unsupported() -> Error {
624 io::Error::new(
625 io::ErrorKind::Unsupported,
626 "Snowbound can't change this notebook's folders in another app yet",
627 )
628 .into()
629}
630
631impl Storage for Tree {
632 /// Of several documents with one name, the first by id stands for it and the rest are
633 /// unavailable, named with their ids.
634 fn discover(
635 &self,
636 cache: &mut discover::Cache,
637 limits: discover::Limits,
638 ) -> Result<discover::Folder> {
639 let kept = crate::fs::read(&self.listed)
640 .ok()
641 .and_then(|bytes| serde_json::from_slice(&bytes).ok())
642 .unwrap_or_default();
643 let mut source = Source {
644 documents: &self.documents,
645 kept,
646 listed: BTreeMap::new(),
647 duplicates: BTreeMap::new(),
648 };
649 let mut catalog = cache.discover(&mut source, limits)?;
650 if let Ok(json) = serde_json::to_vec(&source.listed) {
651 let _ = crate::fs::write(&self.listed, json);
652 }
653 for (path, duplicates) in source.duplicates {
654 let Some(folder) = folder(&mut catalog, &path) else {
655 continue;
656 };
657 for duplicate in duplicates {
658 folder.unavailable.push(discover::Unavailable {
659 path: discover::join(&path, &format!("{} ({})", duplicate.name, duplicate.id)),
660 group: duplicate.directory,
661 error: "Another document here has the same name".into(),
662 reason: discover::Reason::Denied,
663 });
664 }
665 }
666 Ok(catalog)
667 }
668
669 fn location(&self) -> String {
670 self.documents.location()
671 }
672
673 fn entries(&self, folder: &str) -> io::Result<Vec<discover::Entry>> {
674 self.documents.entries(folder)
675 }
676
677 fn exists(&self, path: &str) -> bool {
678 let (folder, name) = path.rsplit_once('/').unwrap_or(("", path));
679 self.documents
680 .entries(folder)
681 .is_ok_and(|entries| entries.iter().any(|entry| entry.name == name))
682 }
683
684 fn stamp(&self, path: &str) -> io::Result<Stamp> {
685 Ok(self.documents.stamp(path)?.0)
686 }
687
688 fn read(&self, path: &str) -> Result<Vec<u8>> {
689 Ok(self.documents.read(path, crate::MAX_FILE_BYTES)?)
690 }
691
692 fn read_file(&self, path: &str, limit: usize) -> Result<Vec<u8>> {
693 Ok(self.documents.read(path, limit)?)
694 }
695
696 fn create(&self, _: &str, _: &[u8]) -> Result<()> {
697 Err(unsupported())
698 }
699
700 fn create_directory(&self, _: &str) -> Result<()> {
701 Err(unsupported())
702 }
703
704 fn hide(&self, _: &str) -> Result<()> {
705 Err(unsupported())
706 }
707
708 fn rename(&self, from: &str, to: &str) -> Result<()> {
709 let folder = |path: &str| path.rsplit_once('/').map(|(folder, _)| folder.to_owned());
710 if folder(from) != folder(to) {
711 return Err(unsupported());
712 }
713 let name = to.rsplit('/').next().unwrap_or(to);
714 Ok(self.documents.rename(from, name)?)
715 }
716
717 fn rename_root(&self, _: &str, _: &[String]) -> Result<String> {
718 Err(unsupported())
719 }
720
721 fn replace(&self, _: &str, _: &str) -> Result<()> {
722 Err(unsupported())
723 }
724
725 fn delete(&self, _: &str) -> Result<()> {
726 Err(unsupported())
727 }
728
729 fn place(&self, path: &str, ancestor: [u8; 16], name: &str) -> Result<()> {
730 let place = |io: &mut Io| {
731 onestore::place(io, ancestor, name).map_err(|error| CommitError {
732 state: CommitState::Unknown,
733 error,
734 })
735 };
736 Ok(self.documents.write(path, place, |_| Ok(()))?)
737 }
738
739 fn commit(&self, path: &str, transaction: &Transaction) -> Result<()> {
740 Ok(self
741 .documents
742 .write(path, |io| transaction.commit(io), |_| Ok(()))?)
743 }
744
745 fn confirm(&self, path: &str, base: &Stamp) -> std::result::Result<(), CommitError> {
746 self.documents
747 .write(path, |io| onestore::confirm(io, base), |_| Ok(()))
748 }
749
750 fn supersede(&self, _: &str, _: &Stamp, _: &str) -> Result<()> {
751 Err(unsupported())
752 }
753}
754
755fn folder<'a>(folder: &'a mut discover::Folder, path: &str) -> Option<&'a mut discover::Folder> {
756 if folder.path == path {
757 return Some(folder);
758 }
759 folder
760 .groups
761 .iter_mut()
762 .find_map(|group| self::folder(group, path))
763}
764
765#[cfg(all(test, unix))]
766mod tests;