sync from monorepo @ 2452e92e
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
//! Platform-native configuration and data path resolution for Dirigent.
|
||||
//!
|
||||
//! Provides `DirigentPaths` for resolving config and data directories
|
||||
//! across Linux, macOS, and Windows. On Linux/macOS, creates a symlink
|
||||
//! from config_dir/data to data_dir for discoverability.
|
||||
|
||||
mod paths;
|
||||
|
||||
pub use paths::ConfigPathError;
|
||||
pub use paths::DirigentPaths;
|
||||
@@ -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") };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user