| ... | ... | @@ -278,7 +278,7 @@ struct Checked { |
| 278 | 278 | } |
| 279 | 279 | |
| 280 | 280 | pub struct Library { |
| 281 | | /// `None` for a lone section. |
| 281 | /// A share's catalog may be absent while cached sections remain available. |
| 282 | 282 | notebook: Mutex<Option<Notebook>>, |
| 283 | 283 | place: Place, |
| 284 | 284 | cache: PathBuf, |
| ... | ... | @@ -1020,14 +1020,14 @@ impl Section { |
| 1020 | 1020 | Ok(space) |
| 1021 | 1021 | } |
| 1022 | 1022 | |
| 1023 | | /// Deletes a page as OneNote 2010 does, keeping a copy in the notebook's recycle bin; |
| 1024 | | /// a conflict page goes for good. A section left empty gets a new page. |
| 1023 | /// Recycles an ordinary notebook page before submitting its deletion; a lone section's |
| 1024 | /// page or a conflict page goes for good. A section left empty gets a new page. |
| 1025 | 1025 | pub(crate) fn delete_page(&self, space: ExGuid, date: &str, time: &str) -> Result<()> { |
| 1026 | 1026 | let section = &self.shared.section; |
| 1027 | 1027 | let listed = section.pages()?; |
| 1028 | 1028 | let mut ops = vec![Op::Section(SectionOp::Delete(vec![space]))]; |
| 1029 | 1029 | if listed.iter().any(|(page, ..)| *page == space) { |
| 1030 | | if self.library.notebook().is_some() { |
| 1030 | if !matches!(self.library.place, Place::File(_)) { |
| 1031 | 1031 | let page = section.page(space)?; |
| 1032 | 1032 | let author = self.shared.author(); |
| 1033 | 1033 | self.library.with_notebook(true, |notebook| { |
| ... | ... | @@ -2351,9 +2351,9 @@ pub unsafe extern "C" fn sb_section_set_theme( |
| 2351 | 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 |
| 2355 | | /// left without pages gets one titled `date` and `time`. Writes to the notebook, so call it |
| 2356 | | /// off the main thread. |
| 2354 | /// Recycles notebook page `id` before submitting its deletion; a lone section's page or a |
| 2355 | /// conflict page goes for good. A section left empty gets one titled `date` and `time`. |
| 2356 | /// Writes to the notebook, so call it off the main thread. |
| 2357 | 2357 | /// |
| 2358 | 2358 | /// # Safety |
| 2359 | 2359 | /// Every string is NUL-terminated UTF-8. |
| ... | ... | @@ -2437,3 +2437,91 @@ pub extern "C" fn sb_section_wake(section: &Section) { |
| 2437 | 2437 | pub unsafe extern "C" fn sb_section_free(section: *mut Section) { |
| 2438 | 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 | } |