use std::{
path::{Path, PathBuf},
sync::Arc,
};
use crate::{
cache::Cache,
error::{LogoxideError, Result},
lock::{LockedPackage, Lockfile},
manifest::MANIFEST,
package::{Features, Package},
resolve::Packages,
storage::{FsStorage, Storage},
};
#[derive(Clone, Debug)]
pub struct Library {
root: PathBuf,
cache: PathBuf,
}
pub(crate) fn env_dir(var: &str) -> Option<PathBuf> {
std::env::var_os(var)
.filter(|v| !v.is_empty())
.map(PathBuf::from)
}
fn home() -> PathBuf {
env_dir("HOME").unwrap_or_else(|| PathBuf::from("."))
}
impl Library {
pub fn new(root: impl Into<PathBuf>, cache: impl Into<PathBuf>) -> Self {
Self {
root: root.into(),
cache: cache.into(),
}
}
pub fn default_root() -> PathBuf {
env_dir("LOGOXIDE_HOME")
.or_else(|| env_dir("XDG_DATA_HOME").map(|d| d.join("logoxide")))
.unwrap_or_else(|| home().join(".local/share/logoxide"))
}
pub fn default_cache() -> PathBuf {
env_dir("LOGOXIDE_CACHE")
.or_else(|| env_dir("XDG_CACHE_HOME").map(|d| d.join("logoxide")))
.unwrap_or_else(|| home().join(".cache/logoxide"))
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn cache_dir(&self) -> &Path {
&self.cache
}
pub fn package_dir(&self, locked: &LockedPackage, base: &Path) -> PathBuf {
match &locked.path {
Some(path) => base.join(path),
None => self
.root
.join("packages")
.join(&locked.id)
.join(locked.key()),
}
}
pub fn is_installed(&self, locked: &LockedPackage, base: &Path) -> bool {
self.package_dir(locked, base).join(MANIFEST).is_file()
}
pub fn installed(&self) -> Vec<(String, String)> {
let mut found = vec![];
let Ok(ids) = std::fs::read_dir(self.root.join("packages")) else {
return found;
};
for id in ids.flatten() {
let Ok(versions) = std::fs::read_dir(id.path()) else {
continue;
};
for version in versions.flatten() {
if version.path().join(MANIFEST).is_file() {
found.push((
id.file_name().to_string_lossy().into_owned(),
version.file_name().to_string_lossy().into_owned(),
));
}
}
}
found.sort();
found
}
pub fn open(&self, locked: &LockedPackage, base: &Path) -> Result<Package> {
let dir = self.package_dir(locked, base);
if !dir.join(MANIFEST).is_file() {
return Err(LogoxideError::NotFound(format!(
"{} (not installed: run `lx install`)",
locked.name()
)));
}
open_locked(Arc::new(FsStorage::new(dir)), locked)
}
pub fn packages<'a>(&'a self, base: &'a Path) -> LibraryPackages<'a> {
LibraryPackages {
library: self,
base,
}
}
pub fn book_cache(&self) -> Cache {
Cache::new(Arc::new(FsStorage::new(self.cache.join("books"))))
.with_stamps(Arc::new(FsStorage::new(self.cache.join("stamps"))))
}
pub fn git_dir(&self) -> PathBuf {
self.cache.join("git")
}
pub fn roots_dir(&self) -> PathBuf {
self.root.join(crate::roots::ROOTS_DIR)
}
pub fn record_root(&self, project: &Path, lock: &Lockfile) -> Result<()> {
crate::roots::write_root(&self.roots_dir(), &crate::roots::Root::of(project, lock))
}
pub fn roots(&self) -> Result<Vec<crate::roots::Root>> {
crate::roots::read_roots(&self.roots_dir())
}
pub fn lock(&self) -> Result<crate::locking::Guard> {
crate::locking::lock(&self.root, "the library at")
}
pub fn stale_staging(&self, max_age: std::time::Duration) -> Vec<PathBuf> {
let mut found = vec![];
let Ok(ids) = std::fs::read_dir(self.root.join("packages")) else {
return found;
};
let now = std::time::SystemTime::now();
for id in ids.flatten() {
let Ok(entries) = std::fs::read_dir(id.path()) else {
continue;
};
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().into_owned();
let staging = name.starts_with('.') && name.contains(".staging-");
let old = (entry.metadata().and_then(|m| m.modified()).ok())
.and_then(|modified| now.duration_since(modified).ok())
.is_some_and(|age| age >= max_age);
if staging && old && entry.path().is_dir() {
found.push(entry.path());
}
}
}
found.sort();
found
}
}
fn staging_dir(dir: &Path, key: &str) -> PathBuf {
use std::{
hash::{BuildHasher, RandomState},
sync::atomic::{AtomicU64, Ordering},
};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let random = RandomState::new().hash_one(n) & 0xffff_ffff;
dir.with_file_name(format!(
".{key}.staging-{}-{n}-{random:08x}",
std::process::id()
))
}
pub fn open_locked(storage: Arc<dyn Storage>, locked: &LockedPackage) -> Result<Package> {
let features: Vec<&str> = locked.features.iter().map(String::as_str).collect();
Package::open(
storage,
Features {
features: &features,
default: locked.default_features,
},
)
}
pub fn library_dir(locked: &LockedPackage) -> String {
format!("packages/{}/{}", locked.id, locked.key())
}
pub struct LibraryPackages<'a> {
library: &'a Library,
base: &'a Path,
}
impl Packages for LibraryPackages<'_> {
fn installed(&self, locked: &LockedPackage) -> Result<Option<Arc<dyn Storage>>> {
Ok(self
.library
.is_installed(locked, self.base)
.then(|| -> Arc<dyn Storage> {
Arc::new(FsStorage::new(self.library.package_dir(locked, self.base)))
}))
}
fn path_package(&self, relative: &str) -> Result<Arc<dyn Storage>> {
Ok(Arc::new(FsStorage::new(self.base.join(relative))))
}
fn repair(&self, locked: &LockedPackage, files: &dyn Storage) -> Result<()> {
let dir = FsStorage::new(self.library.package_dir(locked, self.base));
let declared = crate::package::declared_files(&dir)?;
let wanted = files.walk("")?;
for path in &wanted {
dir.write(path, &files.read(path)?)?;
}
for path in declared {
if !wanted.contains(&path) {
dir.remove(&path)?;
}
}
Ok(())
}
fn install(&self, locked: &LockedPackage, files: &dyn Storage) -> Result<()> {
let dir = self.library.package_dir(locked, self.base);
let staging = staging_dir(&dir, &locked.key());
let result = (|| {
let to = FsStorage::new(&staging);
for path in files.walk("")? {
to.write(&path, &files.read(&path)?)?;
}
std::fs::rename(&staging, &dir)
.map_err(|e| LogoxideError::io(&dir.display().to_string(), e))
})();
let _ = std::fs::remove_dir_all(&staging);
result
}
}