Library: a library's own file names its folder; any other file is a clear error

`lightcraft-cli run --library <dir>/catalog.snap` failed with the OS's "File exists"
(creating a folder over the file). Session::open_library now opens the folder that holds
catalog.snap / catalog.log / catalog.lock, and says "is a file; a library is a folder"
for any other file.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Andrew FrahnandClaude Opus 5.5 committed 2026-10-11 20:48:36 +11:00
1 parent 0ba118de92
commit 457c899160
1 file changed
+59 -1
+59 -1
View File
@@ -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<PathBuf> {
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<PathBuf> {
@@ -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<Path>, 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<String> {
.to_string()
})
}
#[cfg(test)]
mod resolve_tests {
use super::*;
/// `--library <dir>/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);
}
}