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