use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use super::{DaemonError, Result};
#[derive(Clone, Debug)]
pub struct CachePaths {
pub root: PathBuf,
pub images: PathBuf,
pub catalog_db: PathBuf,
pub history_db: PathBuf,
pub log: PathBuf,
pub log_dir: PathBuf,
pub log_file_name: String,
pub socket: PathBuf,
pub pid_file: PathBuf,
pub index_rkyv: PathBuf,
}
impl CachePaths {
pub fn resolve() -> Result<Self> {
let cache_home = std::env::var_os("XDG_CACHE_HOME")
.map(PathBuf::from)
.or_else(|| dirs::cache_dir())
.ok_or_else(|| DaemonError::other("could not resolve cache home"))?;
let root = cache_home.join("zshrs");
Ok(Self::with_root(root))
}
pub fn with_root<P: Into<PathBuf>>(root: P) -> Self {
let root = root.into();
let images = root.join("images");
let catalog_db = root.join("catalog.db");
let history_db = root.join("history.db");
let log = root.join("zshrs.log");
let log_dir = root.clone();
let log_file_name = "zshrs.log".to_string();
let socket = root.join("daemon.sock");
let pid_file = root.join("daemon.pid");
let index_rkyv = root.join("index.rkyv");
Self {
root,
images,
catalog_db,
history_db,
log,
log_dir,
log_file_name,
socket,
pid_file,
index_rkyv,
}
}
pub fn ensure_dirs(&self) -> Result<()> {
ensure_dir_700(&self.root)?;
ensure_dir_700(&self.images)?;
Ok(())
}
pub fn is_first_run(&self) -> bool {
if self.pid_file.exists() {
return false;
}
if self.index_rkyv.exists() {
return false;
}
if let Ok(mut iter) = std::fs::read_dir(&self.images) {
if iter.next().is_some() {
return false;
}
}
true
}
}
fn ensure_dir_700(path: &Path) -> Result<()> {
if !path.exists() {
std::fs::create_dir_all(path)?;
}
let mut perms = std::fs::metadata(path)?.permissions();
if perms.mode() & 0o777 != 0o700 {
perms.set_mode(0o700);
std::fs::set_permissions(path, perms)?;
}
Ok(())
}
pub fn ensure_file_600(path: &Path) -> Result<()> {
if !path.exists() {
return Ok(());
}
let mut perms = std::fs::metadata(path)?.permissions();
let mode = perms.mode() & 0o777;
if mode != 0o600 {
tracing::warn!(
path = %path.display(),
current_mode = format!("{:o}", mode),
"file mode drift; coercing to 0600"
);
perms.set_mode(0o600);
std::fs::set_permissions(path, perms)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn paths_relative_to_root() {
let tmp = TempDir::new().unwrap();
let p = CachePaths::with_root(tmp.path());
assert!(p.images.starts_with(tmp.path()));
assert_eq!(p.images.file_name().unwrap(), "images");
assert_eq!(p.socket.file_name().unwrap(), "daemon.sock");
assert_eq!(p.pid_file.file_name().unwrap(), "daemon.pid");
}
#[test]
fn ensure_dirs_sets_0700() {
let tmp = TempDir::new().unwrap();
let p = CachePaths::with_root(tmp.path().join("zshrs"));
p.ensure_dirs().unwrap();
let mode = std::fs::metadata(&p.root).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o700);
let mode = std::fs::metadata(&p.images).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o700);
}
#[test]
fn first_run_detected_on_empty_root() {
let tmp = TempDir::new().unwrap();
let p = CachePaths::with_root(tmp.path().join("zshrs"));
p.ensure_dirs().unwrap();
assert!(p.is_first_run());
}
#[test]
fn first_run_false_when_pid_exists() {
let tmp = TempDir::new().unwrap();
let p = CachePaths::with_root(tmp.path().join("zshrs"));
p.ensure_dirs().unwrap();
std::fs::write(&p.pid_file, "12345").unwrap();
assert!(!p.is_first_run());
}
}