envelope_cli/config/
paths.rs

1//! Path management for EnvelopeCLI
2//!
3//! Provides XDG-compliant path resolution for configuration, data, and backups.
4//! Default location: ~/.envelope/
5
6use std::path::PathBuf;
7
8use directories::ProjectDirs;
9
10use crate::error::EnvelopeError;
11
12/// Manages all paths used by EnvelopeCLI
13#[derive(Debug, Clone)]
14pub struct EnvelopePaths {
15    /// Base directory for all EnvelopeCLI data (~/.envelope/)
16    base_dir: PathBuf,
17}
18
19impl EnvelopePaths {
20    /// Create a new EnvelopePaths instance using XDG directories
21    ///
22    /// # Errors
23    ///
24    /// Returns an error if the home directory cannot be determined.
25    pub fn new() -> Result<Self, EnvelopeError> {
26        let base_dir = if let Some(proj_dirs) = ProjectDirs::from("", "", "envelope") {
27            proj_dirs.data_dir().to_path_buf()
28        } else {
29            // Fallback to ~/.envelope if XDG fails
30            dirs_fallback()?
31        };
32
33        Ok(Self { base_dir })
34    }
35
36    /// Create EnvelopePaths with a custom base directory (useful for testing)
37    pub fn with_base_dir(base_dir: PathBuf) -> Self {
38        Self { base_dir }
39    }
40
41    /// Get the base directory (~/.envelope/ or equivalent)
42    pub fn base_dir(&self) -> &PathBuf {
43        &self.base_dir
44    }
45
46    /// Get the config directory (same as base for simplicity)
47    pub fn config_dir(&self) -> PathBuf {
48        self.base_dir.clone()
49    }
50
51    /// Get the data directory (~/.envelope/data/)
52    pub fn data_dir(&self) -> PathBuf {
53        self.base_dir.join("data")
54    }
55
56    /// Get the backup directory (~/.envelope/backups/)
57    pub fn backup_dir(&self) -> PathBuf {
58        self.base_dir.join("backups")
59    }
60
61    /// Get the path to the settings file
62    pub fn settings_file(&self) -> PathBuf {
63        self.base_dir.join("config.json")
64    }
65
66    /// Get the path to the audit log
67    pub fn audit_log(&self) -> PathBuf {
68        self.base_dir.join("audit.log")
69    }
70
71    /// Get the path to accounts.json
72    pub fn accounts_file(&self) -> PathBuf {
73        self.data_dir().join("accounts.json")
74    }
75
76    /// Get the path to transactions.json
77    pub fn transactions_file(&self) -> PathBuf {
78        self.data_dir().join("transactions.json")
79    }
80
81    /// Get the path to budget.json (categories and groups)
82    pub fn budget_file(&self) -> PathBuf {
83        self.data_dir().join("budget.json")
84    }
85
86    /// Get the path to allocations.json (budget allocations per period)
87    pub fn allocations_file(&self) -> PathBuf {
88        self.data_dir().join("allocations.json")
89    }
90
91    /// Get the path to payees.json
92    pub fn payees_file(&self) -> PathBuf {
93        self.data_dir().join("payees.json")
94    }
95
96    /// Get the path to targets.json (budget targets)
97    pub fn targets_file(&self) -> PathBuf {
98        self.data_dir().join("targets.json")
99    }
100
101    /// Ensure all required directories exist
102    ///
103    /// Creates:
104    /// - Base directory (~/.envelope/)
105    /// - Data directory (~/.envelope/data/)
106    /// - Backup directory (~/.envelope/backups/)
107    pub fn ensure_directories(&self) -> Result<(), EnvelopeError> {
108        std::fs::create_dir_all(&self.base_dir)
109            .map_err(|e| EnvelopeError::Io(format!("Failed to create base directory: {}", e)))?;
110
111        std::fs::create_dir_all(self.data_dir())
112            .map_err(|e| EnvelopeError::Io(format!("Failed to create data directory: {}", e)))?;
113
114        std::fs::create_dir_all(self.backup_dir())
115            .map_err(|e| EnvelopeError::Io(format!("Failed to create backup directory: {}", e)))?;
116
117        Ok(())
118    }
119
120    /// Check if EnvelopeCLI has been initialized (config file exists)
121    pub fn is_initialized(&self) -> bool {
122        self.settings_file().exists()
123    }
124}
125
126/// Fallback path resolution when XDG directories aren't available
127fn dirs_fallback() -> Result<PathBuf, EnvelopeError> {
128    let home = std::env::var("HOME")
129        .map_err(|_| EnvelopeError::Config("Could not determine home directory".into()))?;
130    Ok(PathBuf::from(home).join(".envelope"))
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use tempfile::TempDir;
137
138    #[test]
139    fn test_custom_base_dir() {
140        let temp_dir = TempDir::new().unwrap();
141        let paths = EnvelopePaths::with_base_dir(temp_dir.path().to_path_buf());
142
143        assert_eq!(paths.base_dir(), temp_dir.path());
144        assert_eq!(paths.data_dir(), temp_dir.path().join("data"));
145        assert_eq!(paths.backup_dir(), temp_dir.path().join("backups"));
146    }
147
148    #[test]
149    fn test_ensure_directories() {
150        let temp_dir = TempDir::new().unwrap();
151        let paths = EnvelopePaths::with_base_dir(temp_dir.path().to_path_buf());
152
153        paths.ensure_directories().unwrap();
154
155        assert!(paths.data_dir().exists());
156        assert!(paths.backup_dir().exists());
157    }
158
159    #[test]
160    fn test_file_paths() {
161        let temp_dir = TempDir::new().unwrap();
162        let paths = EnvelopePaths::with_base_dir(temp_dir.path().to_path_buf());
163
164        assert_eq!(paths.settings_file(), temp_dir.path().join("config.json"));
165        assert_eq!(
166            paths.accounts_file(),
167            temp_dir.path().join("data").join("accounts.json")
168        );
169    }
170}