|
|
|
@@ -0,0 +1,282 @@
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
use thiserror::Error;
|
|
|
|
|
|
|
|
|
|
#[derive(Error, Debug)]
|
|
|
|
|
pub enum ConfigPathError {
|
|
|
|
|
#[error("Could not determine config directory for this platform")]
|
|
|
|
|
NoConfigDir,
|
|
|
|
|
#[error("Could not determine data directory for this platform")]
|
|
|
|
|
NoDataDir,
|
|
|
|
|
#[error("Failed to create directory {path}: {source}")]
|
|
|
|
|
CreateDir {
|
|
|
|
|
path: PathBuf,
|
|
|
|
|
source: std::io::Error,
|
|
|
|
|
},
|
|
|
|
|
#[error("Failed to create symlink from {from} to {to}: {source}")]
|
|
|
|
|
Symlink {
|
|
|
|
|
from: PathBuf,
|
|
|
|
|
to: PathBuf,
|
|
|
|
|
source: std::io::Error,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Platform-native paths for dirigent configuration and data.
|
|
|
|
|
///
|
|
|
|
|
/// Environment variable overrides (highest priority):
|
|
|
|
|
/// - `DIRIGENT_CONFIG_DIR` -- overrides the config directory
|
|
|
|
|
/// - `DIRIGENT_DATA_DIR` -- overrides the data directory
|
|
|
|
|
///
|
|
|
|
|
/// Platform-native defaults (fallback):
|
|
|
|
|
///
|
|
|
|
|
/// | Platform | Config Dir | Data Dir |
|
|
|
|
|
/// |----------|-----------|----------|
|
|
|
|
|
/// | Linux | `$XDG_CONFIG_HOME/dirigent/` | `$XDG_DATA_HOME/dirigent/` |
|
|
|
|
|
/// | macOS | `~/.config/dirigent/` | `~/Library/Application Support/dirigent/` |
|
|
|
|
|
/// | Windows | `%APPDATA%\dirigent\` | `%LOCALAPPDATA%\dirigent\` |
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct DirigentPaths {
|
|
|
|
|
config_dir: PathBuf,
|
|
|
|
|
data_dir: PathBuf,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DirigentPaths {
|
|
|
|
|
/// Resolve platform-native paths.
|
|
|
|
|
pub fn resolve() -> Result<Self, ConfigPathError> {
|
|
|
|
|
let config_dir = Self::resolve_config_dir()?;
|
|
|
|
|
let data_dir = Self::resolve_data_dir()?;
|
|
|
|
|
Ok(Self {
|
|
|
|
|
config_dir,
|
|
|
|
|
data_dir,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create directories and symlink if they don't exist.
|
|
|
|
|
pub fn ensure_dirs(&self) -> Result<(), ConfigPathError> {
|
|
|
|
|
std::fs::create_dir_all(&self.config_dir).map_err(|e| ConfigPathError::CreateDir {
|
|
|
|
|
path: self.config_dir.clone(),
|
|
|
|
|
source: e,
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
std::fs::create_dir_all(&self.data_dir).map_err(|e| ConfigPathError::CreateDir {
|
|
|
|
|
path: self.data_dir.clone(),
|
|
|
|
|
source: e,
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
let noproject = self.noproject_home_dir();
|
|
|
|
|
std::fs::create_dir_all(&noproject).map_err(|e| ConfigPathError::CreateDir {
|
|
|
|
|
path: noproject,
|
|
|
|
|
source: e,
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
// Create symlink on Linux/macOS: config_dir/data -> data_dir
|
|
|
|
|
#[cfg(unix)]
|
|
|
|
|
{
|
|
|
|
|
let symlink_path = self.config_dir.join("data");
|
|
|
|
|
if symlink_path.symlink_metadata().is_err() {
|
|
|
|
|
std::os::unix::fs::symlink(&self.data_dir, &symlink_path).map_err(|e| {
|
|
|
|
|
ConfigPathError::Symlink {
|
|
|
|
|
from: symlink_path,
|
|
|
|
|
to: self.data_dir.clone(),
|
|
|
|
|
source: e,
|
|
|
|
|
}
|
|
|
|
|
})?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Where `dirigent.toml` lives.
|
|
|
|
|
pub fn config_dir(&self) -> &Path {
|
|
|
|
|
&self.config_dir
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Where archives, projects, caches live.
|
|
|
|
|
pub fn data_dir(&self) -> &Path {
|
|
|
|
|
&self.data_dir
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Archive storage directory.
|
|
|
|
|
pub fn archive_dir(&self) -> PathBuf {
|
|
|
|
|
self.data_dir.join("archives")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Project storage directory.
|
|
|
|
|
pub fn projects_dir(&self) -> PathBuf {
|
|
|
|
|
self.data_dir.join("projects")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Default working directory for connectors when no project is active.
|
|
|
|
|
/// Lives under the data directory alongside other runtime artifacts.
|
|
|
|
|
pub fn noproject_home_dir(&self) -> PathBuf {
|
|
|
|
|
self.data_dir.join("noproject_home")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Task output storage directory.
|
|
|
|
|
pub fn tasks_dir(&self) -> PathBuf {
|
|
|
|
|
self.data_dir.join("tasks")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Log storage directory.
|
|
|
|
|
pub fn logs_dir(&self) -> PathBuf {
|
|
|
|
|
self.data_dir.join("logs")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Main config file path.
|
|
|
|
|
pub fn config_file(&self) -> PathBuf {
|
|
|
|
|
self.config_dir.join("dirigent.toml")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn resolve_config_dir() -> Result<PathBuf, ConfigPathError> {
|
|
|
|
|
// Environment variable override (highest priority)
|
|
|
|
|
if let Ok(dir) = std::env::var("DIRIGENT_CONFIG_DIR") {
|
|
|
|
|
if !dir.trim().is_empty() {
|
|
|
|
|
// Absolutize relative paths against CWD to prevent nested resolution
|
|
|
|
|
// when subprocesses run from different working directories.
|
|
|
|
|
let path = PathBuf::from(&dir);
|
|
|
|
|
if path.is_relative() {
|
|
|
|
|
return Ok(std::env::current_dir()
|
|
|
|
|
.map_err(|_| ConfigPathError::NoConfigDir)?
|
|
|
|
|
.join(path));
|
|
|
|
|
}
|
|
|
|
|
return Ok(path);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Platform-native fallback
|
|
|
|
|
#[cfg(target_os = "macos")]
|
|
|
|
|
{
|
|
|
|
|
dirs::home_dir()
|
|
|
|
|
.map(|d| d.join(".config").join("dirigent"))
|
|
|
|
|
.ok_or(ConfigPathError::NoConfigDir)
|
|
|
|
|
}
|
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
|
|
|
{
|
|
|
|
|
dirs::config_dir()
|
|
|
|
|
.map(|d| d.join("dirigent"))
|
|
|
|
|
.ok_or(ConfigPathError::NoConfigDir)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn resolve_data_dir() -> Result<PathBuf, ConfigPathError> {
|
|
|
|
|
// Environment variable override (highest priority)
|
|
|
|
|
if let Ok(dir) = std::env::var("DIRIGENT_DATA_DIR") {
|
|
|
|
|
if !dir.trim().is_empty() {
|
|
|
|
|
// Absolutize relative paths against CWD to prevent nested resolution
|
|
|
|
|
// when subprocesses run from different working directories.
|
|
|
|
|
let path = PathBuf::from(&dir);
|
|
|
|
|
if path.is_relative() {
|
|
|
|
|
return Ok(std::env::current_dir()
|
|
|
|
|
.map_err(|_| ConfigPathError::NoDataDir)?
|
|
|
|
|
.join(path));
|
|
|
|
|
}
|
|
|
|
|
return Ok(path);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Platform-native fallback
|
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
|
{
|
|
|
|
|
dirs::data_local_dir()
|
|
|
|
|
.map(|d| d.join("dirigent"))
|
|
|
|
|
.ok_or(ConfigPathError::NoDataDir)
|
|
|
|
|
}
|
|
|
|
|
#[cfg(not(target_os = "windows"))]
|
|
|
|
|
{
|
|
|
|
|
dirs::data_dir()
|
|
|
|
|
.map(|d| d.join("dirigent"))
|
|
|
|
|
.ok_or(ConfigPathError::NoDataDir)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_resolve_returns_paths() {
|
|
|
|
|
let paths = DirigentPaths::resolve().expect("should resolve paths");
|
|
|
|
|
assert!(paths.config_dir().to_string_lossy().contains("dirigent"));
|
|
|
|
|
assert!(paths.data_dir().to_string_lossy().contains("dirigent"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_config_file_path() {
|
|
|
|
|
let paths = DirigentPaths::resolve().unwrap();
|
|
|
|
|
let config = paths.config_file();
|
|
|
|
|
assert!(config.to_string_lossy().ends_with("dirigent.toml"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_archive_dir() {
|
|
|
|
|
let paths = DirigentPaths::resolve().unwrap();
|
|
|
|
|
let archive = paths.archive_dir();
|
|
|
|
|
assert!(archive.to_string_lossy().ends_with("archives"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_projects_dir() {
|
|
|
|
|
let paths = DirigentPaths::resolve().unwrap();
|
|
|
|
|
let projects = paths.projects_dir();
|
|
|
|
|
assert!(projects.to_string_lossy().ends_with("projects"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_config_file_under_config_dir() {
|
|
|
|
|
let paths = DirigentPaths::resolve().unwrap();
|
|
|
|
|
assert!(paths.config_file().starts_with(paths.config_dir()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_subdirs_under_data_dir() {
|
|
|
|
|
let paths = DirigentPaths::resolve().unwrap();
|
|
|
|
|
assert!(paths.archive_dir().starts_with(paths.data_dir()));
|
|
|
|
|
assert!(paths.projects_dir().starts_with(paths.data_dir()));
|
|
|
|
|
assert!(paths.logs_dir().starts_with(paths.data_dir()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
use serial_test::serial;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn test_config_dir_override_from_env() {
|
|
|
|
|
let tmp = std::env::temp_dir().join("dirigent_test_config_override");
|
|
|
|
|
unsafe { std::env::set_var("DIRIGENT_CONFIG_DIR", &tmp) };
|
|
|
|
|
let paths = DirigentPaths::resolve().expect("should resolve with env override");
|
|
|
|
|
assert_eq!(paths.config_dir(), tmp.as_path());
|
|
|
|
|
unsafe { std::env::remove_var("DIRIGENT_CONFIG_DIR") };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn test_data_dir_override_from_env() {
|
|
|
|
|
let tmp = std::env::temp_dir().join("dirigent_test_data_override");
|
|
|
|
|
unsafe { std::env::set_var("DIRIGENT_DATA_DIR", &tmp) };
|
|
|
|
|
let paths = DirigentPaths::resolve().expect("should resolve with env override");
|
|
|
|
|
assert_eq!(paths.data_dir(), tmp.as_path());
|
|
|
|
|
unsafe { std::env::remove_var("DIRIGENT_DATA_DIR") };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_noproject_home_under_data_dir() {
|
|
|
|
|
let paths = DirigentPaths::resolve().unwrap();
|
|
|
|
|
assert!(paths.noproject_home_dir().starts_with(paths.data_dir()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn test_both_dir_overrides_from_env() {
|
|
|
|
|
let tmp_config = std::env::temp_dir().join("dirigent_test_both_config");
|
|
|
|
|
let tmp_data = std::env::temp_dir().join("dirigent_test_both_data");
|
|
|
|
|
unsafe { std::env::set_var("DIRIGENT_CONFIG_DIR", &tmp_config) };
|
|
|
|
|
unsafe { std::env::set_var("DIRIGENT_DATA_DIR", &tmp_data) };
|
|
|
|
|
let paths = DirigentPaths::resolve().expect("should resolve with both overrides");
|
|
|
|
|
assert_eq!(paths.config_dir(), tmp_config.as_path());
|
|
|
|
|
assert_eq!(paths.data_dir(), tmp_data.as_path());
|
|
|
|
|
assert!(paths.archive_dir().starts_with(&tmp_data));
|
|
|
|
|
assert!(paths.projects_dir().starts_with(&tmp_data));
|
|
|
|
|
unsafe { std::env::remove_var("DIRIGENT_CONFIG_DIR") };
|
|
|
|
|
unsafe { std::env::remove_var("DIRIGENT_DATA_DIR") };
|
|
|
|
|
}
|
|
|
|
|
}
|