Skip to main content

kasl/libs/
data_storage.rs

1//! Cross-platform data storage path management for application files.
2//!
3//! Provides a unified interface for managing application data storage locations
4//! across different operating systems following OS conventions.
5//!
6//! ## Features
7//!
8//! - **Platform Support**: Windows LocalAppData, macOS Application Support, Linux XDG
9//! - **Automatic Directory Creation**: Creates required directories on first access
10//! - **Permission Handling**: Uses locations where users have write permissions
11//! - **Environment Variable Support**: Respects custom environment overrides
12//! - **Fallback Strategy**: Uses current directory if standard locations fail
13//!
14//! ## Usage
15//!
16//! ```rust
17//! use kasl::libs::data_storage::DataStorage;
18//!
19//! let storage = DataStorage::new();
20//! let db_path = storage.get_path("kasl.db")?;
21//! let config_path = storage.get_path("config.json")?;
22//! ```
23
24use anyhow::Result;
25use serde::Deserialize;
26use std::env::consts::OS;
27use std::env::var;
28use std::path::{Path, PathBuf};
29use std::{fs, str};
30
31// Include compile-time application metadata
32include!(concat!(env!("OUT_DIR"), "/app_metadata.rs"));
33
34/// Cross-platform data storage path manager.
35///
36/// The `DataStorage` struct provides a centralized way to manage file paths
37/// for application data across different operating systems. It encapsulates
38/// platform-specific logic and provides a consistent interface for path
39/// resolution and directory management.
40///
41/// ## Design Philosophy
42///
43/// The storage manager follows these principles:
44/// - **Platform Compliance**: Adheres to OS-specific directory conventions
45/// - **User-Centric**: Stores data in user-accessible locations
46/// - **Predictable**: Provides consistent behavior across platforms
47/// - **Robust**: Handles edge cases and permission issues gracefully
48///
49/// ## Initialization
50///
51/// The base path is determined during construction based on:
52/// 1. Operating system detection
53/// 2. Environment variable resolution
54/// 3. Fallback to safe defaults if needed
55/// 4. Organization and application name incorporation
56///
57/// ## Thread Safety
58///
59/// The struct is designed to be used safely across multiple threads,
60/// as path resolution is deterministic and doesn't modify internal state.
61#[derive(Deserialize, Clone)]
62pub struct DataStorage {
63    /// Base directory path for all application data.
64    ///
65    /// This path includes the platform-specific user data directory,
66    /// organization name, and application name. All application files
67    /// are stored as children of this base path.
68    ///
69    /// The path is resolved once during construction and remains constant
70    /// throughout the lifetime of the instance.
71    base_path: PathBuf,
72}
73
74impl Default for DataStorage {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80impl DataStorage {
81    /// Creates a new DataStorage instance with platform-appropriate base path.
82    ///
83    /// This constructor performs automatic platform detection and constructs
84    /// the appropriate base directory path following OS conventions. It uses
85    /// environment variables where available and falls back to safe defaults.
86    ///
87    /// ## Platform Resolution Logic
88    ///
89    /// The constructor determines the base path using this priority order:
90    /// 1. **Environment Variables**: Uses OS-specific environment variables
91    /// 2. **Fallback Values**: Uses current directory if environment vars fail
92    /// 3. **Path Construction**: Appends organization and application names
93    /// 4. **Validation**: Ensures the resulting path is usable
94    ///
95    /// ## Application Metadata Integration
96    ///
97    /// The method uses compile-time metadata to construct paths:
98    /// - `APP_METADATA_OWNER`: Organization name (e.g., "lacodda")
99    /// - `APP_METADATA_NAME`: Application name (e.g., "kasl")
100    ///
101    /// This ensures consistent branding and path structure across builds.
102    ///
103    /// # Returns
104    ///
105    /// Returns a new `DataStorage` instance configured for the current platform
106    /// and user environment.
107    ///
108    /// # Example
109    ///
110    /// ```rust
111    /// use kasl::libs::data_storage::DataStorage;
112    ///
113    /// // Create platform-specific storage manager
114    /// let storage = DataStorage::new();
115    ///
116    /// // Base path is automatically configured
117    /// println!("Base path: {:?}", storage.base_path);
118    /// ```
119    ///
120    /// ## Environment Variable Usage
121    ///
122    /// - **Windows**: Uses `LOCALAPPDATA` for local application data
123    /// - **macOS**: Uses `HOME` to construct ~/Library/Application Support path
124    /// - **Linux**: Uses `HOME` to construct ~/.local/share path
125    ///
126    /// ## Error Resilience
127    ///
128    /// If environment variables are not available, the constructor:
129    /// - Falls back to current directory (".")
130    /// - Continues with path construction
131    /// - Defers directory creation until first access
132    /// - Allows application to function in restricted environments
133    pub fn new() -> Self {
134        // Determine platform-specific base directory
135        let base_path = match OS {
136            "windows" => {
137                // Windows: Use Local AppData for per-user application data
138                var("LOCALAPPDATA").unwrap_or_else(|_| ".".into())
139            }
140            "macos" => {
141                // macOS: Use Application Support following Apple guidelines
142                var("HOME").unwrap_or_else(|_| ".".into()) + "/Library/Application Support"
143            }
144            _ => {
145                // Linux/Unix: Use XDG-compliant local share directory
146                var("HOME").unwrap_or_else(|_| ".".into()) + "/.local/share"
147            }
148        };
149
150        // Construct full application path with organization and app name
151        let base_path = Path::new(&base_path).join(APP_METADATA_OWNER).join(APP_METADATA_NAME);
152
153        Self { base_path }
154    }
155
156    /// Resolves a filename to a complete path within the application data directory.
157    ///
158    /// This method takes a filename and returns the complete path where that file
159    /// should be stored within the application's data directory. It automatically
160    /// handles directory creation and ensures the path is ready for file operations.
161    ///
162    /// ## Directory Creation
163    ///
164    /// The method ensures that all necessary parent directories exist:
165    /// - Creates the entire directory tree if missing
166    /// - Uses OS-appropriate permissions for new directories
167    /// - Handles concurrent access scenarios safely
168    /// - Provides clear error messages if creation fails
169    ///
170    /// ## Path Construction
171    ///
172    /// The resulting path combines:
173    /// 1. **Base Path**: Platform-specific application data directory
174    /// 2. **Organization**: Namespace isolation (e.g., "lacodda")
175    /// 3. **Application**: Application-specific subdirectory (e.g., "kasl")
176    /// 4. **Filename**: The requested file within the application directory
177    ///
178    /// # Arguments
179    ///
180    /// * `file_name` - Name of the file to resolve to a full path
181    ///
182    /// # Returns
183    ///
184    /// Returns the complete `PathBuf` where the file should be stored,
185    /// or an error if directory creation fails or paths are invalid.
186    ///
187    /// # Example
188    ///
189    /// ```rust
190    /// use kasl::libs::data_storage::DataStorage;
191    ///
192    /// let storage = DataStorage::new();
193    ///
194    /// // Get path for database file
195    /// let db_path = storage.get_path("kasl.db")?;
196    /// // Result: /home/user/.local/share/lacodda/kasl/kasl.db (Linux)
197    /// //         C:\Users\User\AppData\Local\lacodda\kasl\kasl.db (Windows)
198    ///
199    /// // Get path for configuration file
200    /// let config_path = storage.get_path("config.json")?;
201    ///
202    /// // Get path for session cache
203    /// let session_path = storage.get_path(".jira_session_id")?;
204    /// ```
205    ///
206    /// ## File Naming Conventions
207    ///
208    /// The method accepts any valid filename, but common patterns include:
209    /// - **Database files**: `kasl.db`, `backup.db`
210    /// - **Configuration**: `config.json`, `settings.toml`
211    /// - **Cache files**: `.session_id`, `.auth_token`
212    /// - **Process files**: `kasl-watch.pid`
213    /// - **Logs**: `kasl.log`, `debug.log`
214    ///
215    /// ## Error Scenarios
216    ///
217    /// The method can fail in several situations:
218    /// - **Permission Denied**: Insufficient permissions to create directories
219    /// - **Disk Full**: No space available for directory creation
220    /// - **Path Too Long**: Resulting path exceeds OS limits
221    /// - **Invalid Characters**: Filename contains invalid characters for the OS
222    /// - **Read-Only Filesystem**: Target location is mounted read-only
223    ///
224    /// ## Concurrency Safety
225    ///
226    /// The directory creation process is designed to handle concurrent access:
227    /// - Multiple processes can safely call this method simultaneously
228    /// - Directory creation is atomic where supported by the OS
229    /// - Existing directories are not affected by creation attempts
230    /// - Race conditions in directory creation are handled gracefully
231    pub fn get_path(&self, file_name: &str) -> Result<PathBuf> {
232        // Ensure the base directory structure exists
233        if !self.base_path.exists() {
234            fs::create_dir_all(&self.base_path)?;
235        }
236
237        // Construct and return the complete file path
238        Ok(self.base_path.join(file_name))
239    }
240}