| ... | @@ -278,7 +278,7 @@ struct Checked { | ... | @@ -278,7 +278,7 @@ struct Checked { |
| 278 | } | 278 | } |
| 279 | | 279 | |
| 280 | pub struct Library { | 280 | pub struct Library { |
| 281 | /// `None` for a lone section. | 281 | /// A share's catalog may be absent while cached sections remain available. |
| 282 | notebook: Mutex<Option<Notebook>>, | 282 | notebook: Mutex<Option<Notebook>>, |
| 283 | place: Place, | 283 | place: Place, |
| 284 | cache: PathBuf, | 284 | cache: PathBuf, |
| ... | @@ -1020,14 +1020,14 @@ impl Section { | ... | @@ -1020,14 +1020,14 @@ impl Section { |
| 1020 | Ok(space) | 1020 | Ok(space) |
| 1021 | } | 1021 | } |
| 1022 | | 1022 | |
| 1023 | /// Deletes a page as OneNote 2010 does, keeping a copy in the notebook's recycle bin; | 1023 | /// Recycles an ordinary notebook page before submitting its deletion; a lone section's |
| 1024 | /// a conflict page goes for good. A section left empty gets a new page. | 1024 | /// page or a conflict page goes for good. A section left empty gets a new page. |
| 1025 | pub(crate) fn delete_page(&self, space: ExGuid, date: &str, time: &str) -> Result<()> { | 1025 | pub(crate) fn delete_page(&self, space: ExGuid, date: &str, time: &str) -> Result<()> { |
| 1026 | let section = &self.shared.section; | 1026 | let section = &self.shared.section; |
| 1027 | let listed = section.pages()?; | 1027 | let listed = section.pages()?; |
| 1028 | let mut ops = vec![Op::Section(SectionOp::Delete(vec![space]))]; | 1028 | let mut ops = vec![Op::Section(SectionOp::Delete(vec![space]))]; |
| 1029 | if listed.iter().any(|(page, ..)| *page == space) { | 1029 | if listed.iter().any(|(page, ..)| *page == space) { |
| 1030 | if self.library.notebook().is_some() { | 1030 | if !matches!(self.library.place, Place::File(_)) { |
| 1031 | let page = section.page(space)?; | 1031 | let page = section.page(space)?; |
| 1032 | let author = self.shared.author(); | 1032 | let author = self.shared.author(); |
| 1033 | self.library.with_notebook(true, |notebook| { | 1033 | self.library.with_notebook(true, |notebook| { |
| ... | @@ -2351,9 +2351,9 @@ pub unsafe extern "C" fn sb_section_set_theme( | ... | @@ -2351,9 +2351,9 @@ pub unsafe extern "C" fn sb_section_set_theme( |
| 2351 | report(set).is_some() | 2351 | report(set).is_some() |
| 2352 | } | 2352 | } |
| 2353 | | 2353 | |
| 2354 | /// Deletes page `id` to the notebook's recycle bin, or a conflict page for good; a section | 2354 | /// Recycles notebook page `id` before submitting its deletion; a lone section's page or a |
| 2355 | /// left without pages gets one titled `date` and `time`. Writes to the notebook, so call it | 2355 | /// conflict page goes for good. A section left empty gets one titled `date` and `time`. |
| 2356 | /// off the main thread. | 2356 | /// Writes to the notebook, so call it off the main thread. |
| 2357 | /// | 2357 | /// |
| 2358 | /// # Safety | 2358 | /// # Safety |
| 2359 | /// Every string is NUL-terminated UTF-8. | 2359 | /// Every string is NUL-terminated UTF-8. |
| ... | @@ -2437,3 +2437,91 @@ pub extern "C" fn sb_section_wake(section: &Section) { | ... | @@ -2437,3 +2437,91 @@ pub extern "C" fn sb_section_wake(section: &Section) { |
| 2437 | pub unsafe extern "C" fn sb_section_free(section: *mut Section) { | 2437 | pub unsafe extern "C" fn sb_section_free(section: *mut Section) { |
| 2438 | drop(unsafe { Box::from_raw(section) }); | 2438 | drop(unsafe { Box::from_raw(section) }); |
| 2439 | } | 2439 | } |
| | 2440 | |
| | 2441 | #[cfg(test)] |
| | 2442 | mod tests { |
| | 2443 | use super::*; |
| | 2444 | |
| | 2445 | #[test] |
| | 2446 | fn deleting_a_cold_offline_share_page_preserves_its_source_and_queue() { |
| | 2447 | let directory = tempfile::tempdir().unwrap(); |
| | 2448 | let root = directory.path().join("notebook"); |
| | 2449 | let cache = directory.path().join("cache"); |
| | 2450 | std::fs::create_dir(&root).unwrap(); |
| | 2451 | let bytes = |
| | 2452 | onestore::create_section("Read.one", "Offline source content", "Fixture").unwrap(); |
| | 2453 | std::fs::write(root.join("Read.one"), &bytes).unwrap(); |
| | 2454 | let notebook = Notebook::open(&root, &cache).unwrap(); |
| | 2455 | let mut listed = Listed { |
| | 2456 | tabs: Vec::new(), |
| | 2457 | files: BTreeMap::new(), |
| | 2458 | }; |
| | 2459 | tabs(notebook.catalog(), &HashMap::new(), &mut listed.tabs); |
| | 2460 | files(notebook.catalog(), &mut listed.files); |
| | 2461 | let identity = listed.files["Read.one"].clone(); |
| | 2462 | drop(notebook); |
| | 2463 | |
| | 2464 | let address = "127.0.0.1:0"; |
| | 2465 | let location = notebook::location::smb(address, "agent", "Notebook"); |
| | 2466 | let replicas = notebook::location::folder(&cache, &location); |
| | 2467 | std::fs::create_dir_all(&replicas).unwrap(); |
| | 2468 | let original = replicas.join(format!("{identity}.sqlite")); |
| | 2469 | let replica = Replica::create(&original, &bytes).unwrap(); |
| | 2470 | let space = replica.pages().unwrap()[0].0; |
| | 2471 | replica |
| | 2472 | .apply( |
| | 2473 | "Fixture", |
| | 2474 | Edit { |
| | 2475 | at: filetime(), |
| | 2476 | ops: vec![Op::Page { |
| | 2477 | space, |
| | 2478 | op: onestore::op::PageOp::Color(Some(0x00ef_cdab)), |
| | 2479 | }], |
| | 2480 | }, |
| | 2481 | ) |
| | 2482 | .unwrap(); |
| | 2483 | let page = replica.page(space).unwrap(); |
| | 2484 | let pending = replica.pending().unwrap(); |
| | 2485 | assert_eq!(pending.len(), 1); |
| | 2486 | drop(replica); |
| | 2487 | let listing = cache.join("smb"); |
| | 2488 | std::fs::create_dir_all(&listing).unwrap(); |
| | 2489 | std::fs::write( |
| | 2490 | listing.join("127_0_0_1_0_agent_Notebook.json"), |
| | 2491 | serde_json::to_vec(&listed).unwrap(), |
| | 2492 | ) |
| | 2493 | .unwrap(); |
| | 2494 | |
| | 2495 | let library = Arc::new( |
| | 2496 | Library::server( |
| | 2497 | Server { |
| | 2498 | address: address.into(), |
| | 2499 | share: "agent".into(), |
| | 2500 | user: String::new(), |
| | 2501 | password: String::new(), |
| | 2502 | domain: String::new(), |
| | 2503 | }, |
| | 2504 | "Notebook", |
| | 2505 | &cache, |
| | 2506 | ) |
| | 2507 | .unwrap(), |
| | 2508 | ); |
| | 2509 | sb_library_set_offline(&library, true); |
| | 2510 | assert!(library.notebook().is_none()); |
| | 2511 | let section = |
| | 2512 | Section::open(Arc::clone(&library), "Read.one", "Fixture".into(), || {}).unwrap(); |
| | 2513 | assert_eq!(section.shared.section.page(space).unwrap(), page); |
| | 2514 | assert_eq!(section.shared.section.pending().unwrap(), pending); |
| | 2515 | assert_eq!( |
| | 2516 | hex(&section.shared.section.replica().identity().unwrap()), |
| | 2517 | identity |
| | 2518 | ); |
| | 2519 | |
| | 2520 | assert!(section.delete_page(space, "Date", "Time").is_err()); |
| | 2521 | section.shared.section.written().unwrap(); |
| | 2522 | assert!(library.notebook().is_none()); |
| | 2523 | assert_eq!(section.shared.section.page(space).unwrap(), page); |
| | 2524 | assert_eq!(section.shared.section.pending().unwrap(), pending); |
| | 2525 | assert_eq!(std::fs::read(root.join("Read.one")).unwrap(), bytes); |
| | 2526 | } |
| | 2527 | } |