diff --git a/crates/engine/src/library.rs b/crates/engine/src/library.rs index 4c8bf28aa..683c2b50c 100644 --- a/crates/engine/src/library.rs +++ b/crates/engine/src/library.rs @@ -32,6 +32,32 @@ use crate::{EngineError, LibrarySource, Result, Selection, Session}; /// Library directory name inside the user's Pictures folder. pub const DEFAULT_NAME: &str = "LightCraft Library"; +/// The library folder `path` names. A library is a folder; naming one of its own files +/// (`catalog.snap`, `catalog.log`, `catalog.lock`) means the folder that holds it. Any other +/// existing file is an error that says so, instead of the OS's "File exists" from creating the +/// folder. +pub fn resolve_dir(path: &Path) -> Result { + if !path.is_file() { + return Ok(path.to_path_buf()); + } + let own = path + .file_name() + .and_then(|n| n.to_str()) + .is_some_and(|n| [lightcraft_catalog::journal::SNAPSHOT, lightcraft_catalog::journal::LOG, lightcraft_catalog::lock::LOCK].contains(&n)); + if !own { + return Err(EngineError::Other(format!("{} is a file; a library is a folder", path.display()))); + } + Ok(match path.parent().filter(|p| !p.as_os_str().is_empty()) { + Some(dir) => dir.to_path_buf(), + None => PathBuf::from("."), + }) +} + +/// Whether `dir` already holds a library (its catalog files). +pub fn is_library(dir: &Path) -> bool { + dir.join(lightcraft_catalog::journal::SNAPSHOT).is_file() || dir.join(lightcraft_catalog::journal::LOG).is_file() +} + /// The default library location: `$LIGHTCRAFT_LIBRARY` if set, else `~/Pictures/LightCraft Library` /// (`%USERPROFILE%\Pictures\LightCraft Library` on Windows). pub fn default_dir() -> Option { @@ -253,7 +279,7 @@ impl Session { /// has it open, this fails with [`EngineError::LibraryInUse`] and nothing is read or changed. /// Reopening the library this session already has open keeps its lock. pub fn open_library(&mut self, dir: impl AsRef, seed_demo: bool) -> Result<&LoadReport> { - let dir = dir.as_ref().to_path_buf(); + let dir = resolve_dir(dir.as_ref())?; let open = || FsStore::open(&dir).map_err(|e| EngineError::Other(format!("can't open library {}: {e}", dir.display()))); let stores = LibraryStores { catalog: Box::new(open()?), files: Box::new(open()?), on_disk: true, dir: dir.clone() }; // the library open in this session (same directory): hand its lock over @@ -655,3 +681,35 @@ fn unlocked_warning(lock: Option<&LibraryLock>) -> Option { .to_string() }) } + +#[cfg(test)] +mod resolve_tests { + use super::*; + + /// `--library /catalog.snap` failed with the OS's "File exists" (creating a folder over + /// the file); it opens the library that file belongs to. + #[test] + fn a_librarys_own_file_opens_its_folder_and_any_other_file_is_a_clear_error() { + let dir = std::env::temp_dir().join(format!("lc-resolve-lib-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let mut s = Session::new().with_fs(); + s.open_library(&dir, false).unwrap(); + s.close_library().unwrap(); + drop(s); + assert!(is_library(&dir)); + let snap = dir.join(lightcraft_catalog::journal::SNAPSHOT); + assert_eq!(resolve_dir(&snap).unwrap(), dir); + let mut s = Session::new().with_fs(); + s.open_library(&snap, false).unwrap(); + assert_eq!(s.library.as_ref().map(|l| l.dir.clone()), Some(dir.clone())); + drop(s); + + let other = dir.join("notes.txt"); + std::fs::write(&other, b"x").unwrap(); + let e = Session::new().with_fs().open_library(&other, false).err().unwrap().to_string(); + assert!(e.contains("is a file"), "{e}"); + assert!(!e.contains("File exists"), "{e}"); + let _ = std::fs::remove_dir_all(&dir); + } +}