use crate::error::PathError;
use crate::expand::{ExpandOptions, expand_input};
use crate::inspect::is_existing_directory;
use crate::internal::validation::validate_app_name;
use crate::normalize::normalize;
use crate::resolve::absolute;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlatformDirs {
pub home: PathBuf,
pub config: PathBuf,
pub data: PathBuf,
pub cache: PathBuf,
pub state: Option<PathBuf>,
pub runtime: Option<PathBuf>,
pub temp: PathBuf,
}
pub fn platform_dirs() -> Result<PlatformDirs, PathError> {
let home = dirs::home_dir().ok_or(PathError::HomeDirectoryUnavailable)?;
let config = dirs::config_dir()
.ok_or_else(|| PathError::invalid("config directory is unavailable on this platform"))?;
let data = dirs::data_dir()
.ok_or_else(|| PathError::invalid("data directory is unavailable on this platform"))?;
let cache = dirs::cache_dir()
.ok_or_else(|| PathError::invalid("cache directory is unavailable on this platform"))?;
let state = dirs::state_dir();
let runtime = dirs::runtime_dir();
let temp = std::env::temp_dir();
Ok(PlatformDirs {
home,
config,
data,
cache,
state,
runtime,
temp,
})
}
pub fn config_dir(app_name: &str) -> Result<PathBuf, PathError> {
validate_app_name(app_name)?;
let base = dirs::config_dir()
.ok_or_else(|| PathError::invalid("config directory is unavailable on this platform"))?;
Ok(base.join(app_name))
}
pub fn data_dir(app_name: &str) -> Result<PathBuf, PathError> {
validate_app_name(app_name)?;
let base = dirs::data_dir()
.ok_or_else(|| PathError::invalid("data directory is unavailable on this platform"))?;
Ok(base.join(app_name))
}
pub fn cache_dir(app_name: &str) -> Result<PathBuf, PathError> {
validate_app_name(app_name)?;
let base = dirs::cache_dir()
.ok_or_else(|| PathError::invalid("cache directory is unavailable on this platform"))?;
Ok(base.join(app_name))
}
pub fn temp_dir() -> PathBuf {
std::env::temp_dir()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AppPaths {
pub application_name: String,
pub root_dir: PathBuf,
pub config_dir: PathBuf,
pub data_dir: PathBuf,
pub cache_dir: PathBuf,
pub state_dir: Option<PathBuf>,
pub temp_dir: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum AppRootPolicy {
PlatformDefault,
EnvironmentOverride {
variable: String,
},
Explicit {
path: PathBuf,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AppPathsOptions {
pub application_name: String,
pub environment_override: Option<String>,
pub create_directories: bool,
pub root_policy: Option<AppRootPolicy>,
}
impl AppPathsOptions {
pub fn new(application_name: impl Into<String>) -> Self {
Self {
application_name: application_name.into(),
environment_override: None,
create_directories: false,
root_policy: None,
}
}
}
pub fn app_paths(application_name: &str) -> Result<AppPaths, PathError> {
app_paths_with_options(AppPathsOptions {
application_name: application_name.to_owned(),
environment_override: None,
create_directories: false,
root_policy: Some(AppRootPolicy::PlatformDefault),
})
}
pub fn app_paths_with_options(options: AppPathsOptions) -> Result<AppPaths, PathError> {
validate_app_name(&options.application_name)?;
let policy = options.root_policy.clone().unwrap_or_else(|| {
if let Some(var) = &options.environment_override {
AppRootPolicy::EnvironmentOverride {
variable: var.clone(),
}
} else {
AppRootPolicy::PlatformDefault
}
});
let mut paths = match policy {
AppRootPolicy::PlatformDefault => platform_app_paths(&options.application_name)?,
AppRootPolicy::Explicit { path } => rooted_app_paths(&options.application_name, path)?,
AppRootPolicy::EnvironmentOverride { variable } => match env::var(&variable) {
Ok(value) if !value.trim().is_empty() => {
let expanded = expand_input(
value.trim(),
&ExpandOptions {
reject_undefined_variables: true,
..ExpandOptions::default()
},
)?;
let abs = if expanded.is_absolute() {
normalize(expanded)?
} else {
absolute(expanded)?
};
rooted_app_paths(&options.application_name, abs)?
}
Ok(_) | Err(env::VarError::NotPresent) => {
platform_app_paths(&options.application_name)?
}
Err(env::VarError::NotUnicode(_)) => {
return Err(PathError::invalid(format!(
"environment variable {variable} is not valid Unicode"
)));
}
},
};
if options.create_directories {
create_app_directories(&paths)?;
} else {
if paths.root_dir.exists() && !is_existing_directory(&paths.root_dir) {
return Err(PathError::invalid(format!(
"application root exists and is not a directory: {}",
paths.root_dir.to_string_lossy()
)));
}
}
if options.create_directories && !is_existing_directory(&paths.root_dir) {
return Err(PathError::invalid(format!(
"failed to materialize application root as a directory: {}",
paths.root_dir.to_string_lossy()
)));
}
let _ = &mut paths;
Ok(paths)
}
pub fn app_paths_with_policy(
application_name: &str,
policy: AppRootPolicy,
create_directories: bool,
) -> Result<AppPaths, PathError> {
app_paths_with_options(AppPathsOptions {
application_name: application_name.to_owned(),
environment_override: None,
create_directories,
root_policy: Some(policy),
})
}
fn platform_app_paths(application_name: &str) -> Result<AppPaths, PathError> {
let data = data_dir(application_name)?;
let config = config_dir(application_name)?;
let cache = cache_dir(application_name)?;
let state = dirs::state_dir().map(|s| s.join(application_name));
Ok(AppPaths {
application_name: application_name.to_owned(),
root_dir: data.clone(),
config_dir: config,
data_dir: data,
cache_dir: cache,
state_dir: state,
temp_dir: temp_dir(),
})
}
fn rooted_app_paths(application_name: &str, root: PathBuf) -> Result<AppPaths, PathError> {
let root = normalize(root)?;
if root.as_os_str().is_empty() {
return Err(PathError::EmptyInput);
}
Ok(AppPaths {
application_name: application_name.to_owned(),
config_dir: root.clone(),
data_dir: root.clone(),
cache_dir: root.join("cache"),
state_dir: Some(root.join("state")),
temp_dir: temp_dir(),
root_dir: root,
})
}
fn create_app_directories(paths: &AppPaths) -> Result<(), PathError> {
for dir in [
Some(paths.root_dir.as_path()),
Some(paths.config_dir.as_path()),
Some(paths.data_dir.as_path()),
Some(paths.cache_dir.as_path()),
paths.state_dir.as_deref(),
]
.into_iter()
.flatten()
{
create_dir_if_needed(dir)?;
}
Ok(())
}
fn create_dir_if_needed(path: &Path) -> Result<(), PathError> {
if path.exists() {
if is_existing_directory(path) {
return Ok(());
}
return Err(PathError::invalid(format!(
"path exists and is not a directory: {}",
path.to_string_lossy()
)));
}
fs::create_dir_all(path).map_err(|e| PathError::filesystem(path, e))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn platform_dirs_available() {
let d = platform_dirs().unwrap();
assert!(!d.home.as_os_str().is_empty());
assert!(!d.temp.as_os_str().is_empty());
}
#[test]
fn app_name_validation() {
assert!(config_dir("").is_err());
assert!(config_dir("a/b").is_err());
assert!(config_dir("..").is_err());
assert!(config_dir("CON").is_err());
assert!(config_dir("my-app").is_ok());
}
#[test]
fn app_paths_platform() {
let p = app_paths("path-rs-test-app").unwrap();
assert_eq!(p.application_name, "path-rs-test-app");
assert!(p.root_dir.ends_with("path-rs-test-app"));
}
#[test]
fn app_paths_explicit_and_create() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("app-root");
let p = app_paths_with_policy(
"my-tool",
AppRootPolicy::Explicit { path: root.clone() },
true,
)
.unwrap();
assert_eq!(p.root_dir, normalize(&root).unwrap());
assert!(p.root_dir.is_dir());
assert!(p.cache_dir.is_dir());
}
}