| ... | @@ -71,6 +71,17 @@ pub fn in_icloud_folder(location: &str) -> bool { | ... | @@ -71,6 +71,17 @@ pub fn in_icloud_folder(location: &str) -> bool { |
| 71 | crate::icloud::folder().is_some_and(|root| Path::new(location).parent() == Some(&root)) | 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 | /// `current`, the path of a section, after `from` moved to `to`: a section moved itself, | 85 | /// `current`, the path of a section, after `from` moved to `to`: a section moved itself, |
| 75 | /// or one inside a moved group. | 86 | /// or one inside a moved group. |
| 76 | fn follow(current: &str, from: &str, to: &str) -> String { | 87 | fn follow(current: &str, from: &str, to: &str) -> String { |
| ... | @@ -300,21 +311,18 @@ impl State { | ... | @@ -300,21 +311,18 @@ impl State { |
| 300 | /// Takes `listed`, the notebook folders at the top of the app's iCloud Drive folder, in | 311 | /// Takes `listed`, the notebook folders at the top of the app's iCloud Drive folder, in |
| 301 | /// place of those the sidebar lists from it. | 312 | /// place of those the sidebar lists from it. |
| 302 | fn listed_icloud(&mut self, listed: Vec<String>) { | 313 | fn listed_icloud(&mut self, listed: Vec<String>) { |
| 303 | let gone: Vec<Arc<Library>> = self | 314 | let missed: Vec<String> = (self.notebooks.iter()) |
| 304 | .notebooks | 315 | .map(|library| &library.location) |
| 305 | .iter() | 316 | .filter(|location| { |
| 306 | .filter(|library| { | 317 | in_icloud_folder(location) |
| 307 | in_icloud_folder(&library.location) | 318 | && !listed.contains(location) |
| 308 | && !listed.contains(&library.location) | 319 | && !self.folder_renaming(location) |
| 309 | && !self.folder_renaming(&library.location) | 320 | && !self.trashing.contains(*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() | | |
| 313 | }) | 321 | }) |
| 314 | .cloned() | 322 | .cloned() |
| 315 | .collect(); | 323 | .collect(); |
| 316 | for library in gone { | 324 | for location in missed { |
| 317 | self.close_notebook(&library); | 325 | self.confirm_gone(location); |
| 318 | } | 326 | } |
| 319 | for location in listed { | 327 | for location in listed { |
| 320 | if self.notebooks.iter().any(|open| open.location == location) | 328 | if self.notebooks.iter().any(|open| open.location == location) |
| ... | @@ -348,6 +356,36 @@ impl State { | ... | @@ -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 | /// Whether a folder rename is taking the notebook from or to `location`. | 389 | /// Whether a folder rename is taking the notebook from or to `location`. |
| 352 | pub(crate) fn folder_renaming(&self, location: &str) -> bool { | 390 | pub(crate) fn folder_renaming(&self, location: &str) -> bool { |
| 353 | self.folder_rename | 391 | self.folder_rename |
| ... | @@ -1436,4 +1474,30 @@ mod tests { | ... | @@ -1436,4 +1474,30 @@ mod tests { |
| 1436 | } | 1474 | } |
| 1437 | notebook::fs::remove_dir_all(&temporary).unwrap(); | 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 | } |