use std::io;
use std::path::{Path, PathBuf};
use crate::error::MountError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CacheConvention {
Xdg,
MacOs,
}
impl CacheConvention {
pub fn current() -> Self {
if cfg!(target_os = "macos") {
Self::MacOs
} else {
Self::Xdg
}
}
}
pub fn resolve_spool_dir(
override_dir: Option<&Path>,
xdg_cache_home: Option<&str>,
home: Option<&str>,
convention: CacheConvention,
) -> Result<PathBuf, MountError> {
if let Some(dir) = override_dir {
return Ok(dir.to_path_buf());
}
fn non_empty(v: Option<&str>) -> Option<&str> {
v.filter(|s| !s.is_empty())
}
let home = non_empty(home);
let base = match convention {
CacheConvention::MacOs => home
.map(|h| Path::new(h).join("Library").join("Caches"))
.ok_or_else(|| {
MountError::Other(
"can't find a cache directory: $HOME is not set. \
Pass --spool-dir to say where writes should be spooled."
.into(),
)
})?,
CacheConvention::Xdg => match non_empty(xdg_cache_home) {
Some(xdg) => PathBuf::from(xdg),
None => home.map(|h| Path::new(h).join(".cache")).ok_or_else(|| {
MountError::Other(
"can't find a cache directory: neither $XDG_CACHE_HOME nor $HOME is set. \
Pass --spool-dir to say where writes should be spooled."
.into(),
)
})?,
},
};
Ok(base.join("mtp-mount").join("spool"))
}
pub fn spool_dir_from_env(override_dir: Option<&Path>) -> Result<PathBuf, MountError> {
let xdg = std::env::var("XDG_CACHE_HOME").ok();
let home = std::env::var("HOME").ok();
resolve_spool_dir(
override_dir,
xdg.as_deref(),
home.as_deref(),
CacheConvention::current(),
)
}
pub fn prepare_spool_dir(dir: &Path) -> Result<(), MountError> {
std::fs::create_dir_all(dir).map_err(|e| spool_error(dir, e))?;
tempfile::tempfile_in(dir).map_err(|e| spool_error(dir, e))?;
Ok(())
}
fn spool_error(dir: &Path, source: io::Error) -> MountError {
MountError::Other(format!(
"can't use the spool directory {}: {source}. \
Pass --spool-dir to point it at a writable directory on disk.",
dir.display()
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn xdg_cache_home_wins_when_set() {
let dir = resolve_spool_dir(
None,
Some("/cache"),
Some("/home/dave"),
CacheConvention::Xdg,
)
.unwrap();
assert_eq!(dir, PathBuf::from("/cache/mtp-mount/spool"));
}
#[test]
fn xdg_unset_falls_back_to_home_cache() {
let dir = resolve_spool_dir(None, None, Some("/home/dave"), CacheConvention::Xdg).unwrap();
assert_eq!(dir, PathBuf::from("/home/dave/.cache/mtp-mount/spool"));
}
#[test]
fn empty_xdg_counts_as_unset() {
let dir =
resolve_spool_dir(None, Some(""), Some("/home/dave"), CacheConvention::Xdg).unwrap();
assert_eq!(dir, PathBuf::from("/home/dave/.cache/mtp-mount/spool"));
}
#[test]
fn override_wins_over_env() {
let dir = resolve_spool_dir(
Some(Path::new("/mnt/scratch")),
Some("/cache"),
Some("/home/dave"),
CacheConvention::Xdg,
)
.unwrap();
assert_eq!(dir, PathBuf::from("/mnt/scratch"));
}
#[test]
fn macos_uses_library_caches_and_ignores_xdg() {
let dir = resolve_spool_dir(
None,
Some("/cache"),
Some("/Users/dave"),
CacheConvention::MacOs,
)
.unwrap();
assert_eq!(
dir,
PathBuf::from("/Users/dave/Library/Caches/mtp-mount/spool")
);
}
#[test]
fn no_home_and_no_xdg_errors() {
let err = resolve_spool_dir(None, None, None, CacheConvention::Xdg).unwrap_err();
assert!(err.to_string().contains("--spool-dir"));
let err =
resolve_spool_dir(None, Some("/cache"), None, CacheConvention::MacOs).unwrap_err();
assert!(err.to_string().contains("--spool-dir"));
}
#[test]
fn prepare_creates_missing_directory() {
let parent = tempfile::tempdir().unwrap();
let dir = parent.path().join("mtp-mount").join("spool");
prepare_spool_dir(&dir).unwrap();
assert!(dir.is_dir());
}
#[test]
fn prepare_reports_the_path_it_could_not_use() {
let parent = tempfile::tempdir().unwrap();
let blocked = parent.path().join("not-a-dir");
std::fs::write(&blocked, b"x").unwrap();
let err = prepare_spool_dir(&blocked).unwrap_err().to_string();
assert!(err.contains(&blocked.display().to_string()), "{err}");
}
}