| 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 | |
| 7 | use crate::{Error, Result, discover, session::Storage}; |
| 8 | use onestore::{CommitError, CommitIo, CommitState, Stamp, Store, Transaction}; |
| 9 | use 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)] |
| 20 | pub 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`. |
| 34 | pub 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. |
| 47 | pub 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 | |
| 58 | impl 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 | |
| 270 | fn refused(error: io::Error) -> CommitError { |
| 271 | CommitError { |
| 272 | state: CommitState::NotCommitted, |
| 273 | error, |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | fn uncertain(error: io::Error) -> CommitError { |
| 278 | CommitError { |
| 279 | state: CommitState::Unknown, |
| 280 | error, |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> { |
| 285 | mutex.lock().unwrap_or_else(PoisonError::into_inner) |
| 286 | } |
| 287 | |
| 288 | fn 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. |
| 298 | enum 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. |
| 304 | enum Io { |
| 305 | File(File), |
| 306 | Image(Vec<u8>), |
| 307 | } |
| 308 | |
| 309 | impl 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. |
| 355 | fn 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 | |
| 364 | fn hex(bytes: &[u8]) -> String { |
| 365 | bytes.iter().map(|byte| format!("{byte:02x}")).collect() |
| 366 | } |
| 367 | |
| 368 | fn 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. |
| 375 | struct Kept { |
| 376 | name: String, |
| 377 | document: String, |
| 378 | image: String, |
| 379 | base: String, |
| 380 | } |
| 381 | |
| 382 | impl 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. |
| 397 | const 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. |
| 403 | pub(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 | |
| 410 | impl 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 | |
| 492 | fn parses(image: &[u8]) -> bool { |
| 493 | Store::parse(image).is_ok_and(|store| store.checksum_mismatches.is_empty()) |
| 494 | } |
| 495 | |
| 496 | impl 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`. |
| 585 | pub(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. |
| 591 | struct 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 | |
| 599 | impl 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 | |
| 623 | fn 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 | |
| 631 | impl 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 | |
| 755 | fn 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))] |
| 766 | mod tests; |