use std::fs;
use std::io;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CacheDirOutcome {
Existing,
Created,
Repaired,
}
pub fn ensure_cache_dir(path: &Path) -> io::Result<CacheDirOutcome> {
let mut repaired = false;
match fs::symlink_metadata(path) {
Ok(m) if m.is_dir() => return Ok(CacheDirOutcome::Existing),
Ok(m) if m.file_type().is_symlink() => {
if fs::metadata(path).is_ok_and(|t| t.is_dir()) {
return Ok(CacheDirOutcome::Existing);
}
fs::remove_file(path)?;
repaired = true;
}
Ok(_) => {
fs::remove_file(path)?;
repaired = true;
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => return Err(e),
}
fs::create_dir_all(path)?;
Ok(if repaired {
CacheDirOutcome::Repaired
} else {
CacheDirOutcome::Created
})
}
pub fn user_cache_root() -> Option<PathBuf> {
#[cfg(target_os = "macos")]
{
std::env::var_os("HOME")
.filter(|h| !h.is_empty())
.map(|h| PathBuf::from(h).join("Library").join("Caches"))
}
#[cfg(windows)]
{
std::env::var_os("LOCALAPPDATA")
.filter(|h| !h.is_empty())
.map(PathBuf::from)
}
#[cfg(not(any(target_os = "macos", windows)))]
{
std::env::var_os("XDG_CACHE_HOME")
.map(PathBuf::from)
.filter(|p| p.is_absolute())
.or_else(|| {
std::env::var_os("HOME")
.filter(|h| !h.is_empty())
.map(|h| PathBuf::from(h).join(".cache"))
})
}
}
pub fn ensure_http_cache_dir(identifier: &str) -> io::Result<CacheDirOutcome> {
ensure_http_cache_dir_with(identifier, user_cache_root)
}
pub fn ensure_http_cache_dir_with<F>(identifier: &str, root: F) -> io::Result<CacheDirOutcome>
where
F: FnOnce() -> Option<PathBuf>,
{
let valid = !identifier.is_empty()
&& identifier != "."
&& identifier != ".."
&& !identifier.contains(['/', '\\', '\0']);
if !valid {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"cache identifier must be one path component",
));
}
let root = root().ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, "user cache directory unavailable")
})?;
ensure_cache_dir(&root.join(identifier))
}