| ... | ... | @@ -71,6 +71,17 @@ pub fn in_icloud_folder(location: &str) -> bool { |
| 71 | 71 | crate::icloud::folder().is_some_and(|root| Path::new(location).parent() == Some(&root)) |
| 72 | 72 | } |
| 73 | 73 | |
| 74 | /// How long a notebook folder a listing of the iCloud folder missed must stay missing to count |
| 75 | /// as gone. |
| 76 | const VANISH: std::time::Duration = std::time::Duration::from_secs(3); |
| 77 | |
| 78 | /// Whether the folder at `path` is still not there `after` it was missed: not found, rather |
| 79 | /// than unreadable for a moment, as iCloud Drive's coordinated writes can leave it. |
| 80 | fn vanished(path: &Path, after: std::time::Duration) -> bool { |
| 81 | std::thread::sleep(after); |
| 82 | notebook::fs::metadata(path).is_err_and(|error| error.kind() == std::io::ErrorKind::NotFound) |
| 83 | } |
| 84 | |
| 74 | 85 | /// `current`, the path of a section, after `from` moved to `to`: a section moved itself, |
| 75 | 86 | /// or one inside a moved group. |
| 76 | 87 | fn follow(current: &str, from: &str, to: &str) -> String { |
| ... | ... | @@ -300,21 +311,18 @@ impl State { |
| 300 | 311 | /// Takes `listed`, the notebook folders at the top of the app's iCloud Drive folder, in |
| 301 | 312 | /// place of those the sidebar lists from it. |
| 302 | 313 | fn listed_icloud(&mut self, listed: Vec<String>) { |
| 303 | | let gone: Vec<Arc<Library>> = self |
| 304 | | .notebooks |
| 305 | | .iter() |
| 306 | | .filter(|library| { |
| 307 | | in_icloud_folder(&library.location) |
| 308 | | && !listed.contains(&library.location) |
| 309 | | && !self.folder_renaming(&library.location) |
| 310 | | && !self.trashing.contains(&library.location) |
| 311 | | // A listing taken before a rename names the folder by its old name. |
| 312 | | && notebook::fs::metadata(&library.location).is_err() |
| 314 | let missed: Vec<String> = (self.notebooks.iter()) |
| 315 | .map(|library| &library.location) |
| 316 | .filter(|location| { |
| 317 | in_icloud_folder(location) |
| 318 | && !listed.contains(location) |
| 319 | && !self.folder_renaming(location) |
| 320 | && !self.trashing.contains(*location) |
| 313 | 321 | }) |
| 314 | 322 | .cloned() |
| 315 | 323 | .collect(); |
| 316 | | for library in gone { |
| 317 | | self.close_notebook(&library); |
| 324 | for location in missed { |
| 325 | self.confirm_gone(location); |
| 318 | 326 | } |
| 319 | 327 | for location in listed { |
| 320 | 328 | if self.notebooks.iter().any(|open| open.location == location) |
| ... | ... | @@ -348,6 +356,36 @@ impl State { |
| 348 | 356 | } |
| 349 | 357 | } |
| 350 | 358 | |
| 359 | /// Closes the notebook at `location`, which a listing of the iCloud folder missed, once |
| 360 | /// its folder is still not there `VANISH` later: iCloud Drive's own writes and downloads |
| 361 | /// can hide a folder from a listing for a moment. The notebook shown stays while edits |
| 362 | /// wait in it. |
| 363 | fn confirm_gone(&mut self, location: String) { |
| 364 | if !self.vanishing.insert(location.clone()) { |
| 365 | return; |
| 366 | } |
| 367 | let proxy = self.proxy.clone(); |
| 368 | crate::spawn(move || { |
| 369 | let gone = vanished(Path::new(&location), VANISH); |
| 370 | let _ = proxy.send_event(crate::UserEvent::Then(Box::new(move |state| { |
| 371 | state.vanishing.remove(&location); |
| 372 | let waiting = (state.session.as_ref()) |
| 373 | .filter(|session| session.library.location == location) |
| 374 | .is_some_and(|session| { |
| 375 | session |
| 376 | .section |
| 377 | .pending() |
| 378 | .is_ok_and(|pending| !pending.is_empty()) |
| 379 | }); |
| 380 | let listed = (state.notebooks.iter()).find(|open| open.location == location); |
| 381 | if let Some(library) = listed.cloned().filter(|_| gone && !waiting) { |
| 382 | state.close_notebook(&library); |
| 383 | } |
| 384 | Ok(()) |
| 385 | }))); |
| 386 | }); |
| 387 | } |
| 388 | |
| 351 | 389 | /// Whether a folder rename is taking the notebook from or to `location`. |
| 352 | 390 | pub(crate) fn folder_renaming(&self, location: &str) -> bool { |
| 353 | 391 | self.folder_rename |
| ... | ... | @@ -1436,4 +1474,30 @@ mod tests { |
| 1436 | 1474 | } |
| 1437 | 1475 | notebook::fs::remove_dir_all(&temporary).unwrap(); |
| 1438 | 1476 | } |
| 1477 | |
| 1478 | /// A notebook folder that leaves the iCloud folder for a moment, as iCloud Drive's |
| 1479 | /// writes can take it, is not gone; one that stays away is. |
| 1480 | #[test] |
| 1481 | fn a_folder_missing_for_a_moment_is_not_gone() { |
| 1482 | let temporary = |
| 1483 | std::env::temp_dir().join(format!("snowbound-vanish-{}", std::process::id())); |
| 1484 | let (folder, away) = (temporary.join("Cloudy"), temporary.join(".Cloudy-away")); |
| 1485 | notebook::fs::create_dir_all(&folder).unwrap(); |
| 1486 | notebook::fs::rename(&folder, &away).unwrap(); |
| 1487 | let back = std::thread::spawn({ |
| 1488 | let (folder, away) = (folder.clone(), away.clone()); |
| 1489 | move || { |
| 1490 | std::thread::sleep(std::time::Duration::from_millis(100)); |
| 1491 | notebook::fs::rename(away, folder).unwrap(); |
| 1492 | } |
| 1493 | }); |
| 1494 | assert!(!super::vanished( |
| 1495 | &folder, |
| 1496 | std::time::Duration::from_millis(600) |
| 1497 | )); |
| 1498 | back.join().unwrap(); |
| 1499 | notebook::fs::remove_dir_all(&folder).unwrap(); |
| 1500 | assert!(super::vanished(&folder, std::time::Duration::ZERO)); |
| 1501 | notebook::fs::remove_dir_all(&temporary).unwrap(); |
| 1502 | } |
| 1439 | 1503 | } |