app_data/
app_data.rs

1use std::{
2    env::{self, current_dir, var},
3    fmt, fs,
4    path::PathBuf,
5};
6
7/// Custom error type
8#[derive(Debug, Clone)]
9pub enum AppDataError {
10    /// Environment variable not found
11    EnvVarNotFound(String),
12    /// IO error
13    IoError(String),
14    /// Failed to get current directory
15    CurrentDirError(String),
16}
17
18impl fmt::Display for AppDataError {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            AppDataError::EnvVarNotFound(var) => {
22                write!(f, "Environment variable {} not found", var)
23            }
24            AppDataError::IoError(msg) => {
25                write!(f, "IO error: {}", msg)
26            }
27            AppDataError::CurrentDirError(msg) => {
28                write!(f, "Failed to get current directory: {}", msg)
29            }
30        }
31    }
32}
33
34impl std::error::Error for AppDataError {}
35
36impl From<std::io::Error> for AppDataError {
37    fn from(err: std::io::Error) -> Self {
38        AppDataError::IoError(err.to_string())
39    }
40}
41
42#[cfg(target_os = "windows")]
43pub fn get_sys_app_data_dir() -> Result<PathBuf, AppDataError> {
44    var("APPDATA")
45        .map(PathBuf::from)
46        .map_err(|_| AppDataError::EnvVarNotFound("APPDATA".to_string()))
47}
48
49#[cfg(target_os = "macos")]
50pub fn get_sys_app_data_dir() -> Result<PathBuf, AppDataError> {
51    var("HOME")
52        .map(|home| PathBuf::from(home).join("Library/Application Support"))
53        .map_err(|_| AppDataError::EnvVarNotFound("HOME".to_string()))
54}
55
56#[cfg(target_os = "linux")]
57pub fn get_sys_app_data_dir() -> Result<PathBuf, AppDataError> {
58    if let Ok(xdg) = var("XDG_DATA_HOME") {
59        Ok(PathBuf::from(xdg))
60    } else if let Ok(home) = var("HOME") {
61        Ok(PathBuf::from(home).join(".local/share"))
62    } else {
63        Err(AppDataError::EnvVarNotFound(
64            "XDG_DATA_HOME and HOME".to_string(),
65        ))
66    }
67}
68
69/// # Examples
70///
71/// ```rust
72/// use app_data::AppData;
73///
74/// let app_data = AppData::default();
75///
76/// let app_data = AppData::new("my_app");
77/// ```
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct AppData {
80    /// The name of the application, if the storage location is in the system application directory, use this application name as a subdirectory
81    /// <details><summary><b>中文说明</b></summary>
82    /// 应用名称,如果存储位置在系统应用目录,以此应用名作为子目录
83    /// </details>
84    pub app_name: String,
85    /// Whether to force create the data directory under the startup path
86    /// <details><summary><b>中文说明</b></summary>
87    /// 是否强制在运行目录下创建 data 目录
88    /// </details>
89    pub force_local: bool,
90}
91
92/// Create a new AppData instance
93impl AppData {
94    pub fn new(app_name: &str) -> Self {
95        Self {
96            app_name: app_name.to_string(),
97            force_local: false,
98        }
99    }
100
101    pub fn with_force_local(app_name: &str, force_local: bool) -> Self {
102        Self {
103            app_name: app_name.to_string(),
104            force_local,
105        }
106    }
107}
108
109impl AppData {
110    /// Return the application data directory according to the search rules, ensuring that the directory is valid and exists
111    ///
112    /// <strong>Besides logging, you don't need to care about this method, just keep track of the data file path</strong>
113    ///
114    /// <details><summary><b>中文说明</b></summary>
115    /// 按照寻找规则返回应用数据目录,会确保目录有效且存在。
116    /// <strong>除了记录日志,你并不需要关心此方法,保持关注数据文件路径即可</strong>
117    /// </details>
118    ///
119    /// # Examples
120    ///
121    /// ```rust
122    /// use app_data::AppData;
123    ///
124    /// let app_data = AppData::default();
125    /// let data_dir = app_data.ensure_data_dir().unwrap();
126    /// println!("data_dir: {}", data_dir.display());
127    /// ```
128    pub fn ensure_data_dir(&self) -> Result<PathBuf, AppDataError> {
129        let path = current_dir().map_err(|e| AppDataError::CurrentDirError(e.to_string()))?;
130        let root_path = path.join("data");
131        if root_path.exists() {
132            return Ok(root_path);
133        }
134        if self.force_local {
135            fs::create_dir_all(&root_path)?;
136            return Ok(root_path);
137        }
138        let sys_path = get_sys_app_data_dir()?.join(&self.app_name);
139        if !sys_path.exists() {
140            fs::create_dir_all(&sys_path)?;
141        }
142        Ok(sys_path)
143    }
144
145    /// 获取数据目录中的文件路径
146    ///
147    /// Get the file path in the data directory
148    ///
149    /// # Examples
150    ///
151    /// ```rust
152    /// use app_data::AppData;
153    ///
154    /// let app_data = AppData::new("my_app");
155    /// let file_path = app_data.get_file_path("config.json").unwrap();
156    /// ```
157    pub fn get_file_path(&self, file_name: &str) -> Result<PathBuf, AppDataError> {
158        let data_dir = self.ensure_data_dir()?;
159        Ok(data_dir.join(file_name))
160    }
161}
162
163impl Default for AppData {
164    /// Default using `CARGO_PKG_NAME` as the application name, if `CARGO_PKG_NAME` is not set,
165    /// then `force_local` is true
166    ///
167    /// <details><summary><b>中文说明</b></summary>
168    /// 默认使用 `CARGO_PKG_NAME` 作为应用名称,如果 `CARGO_PKG_NAME` 未设置,则`force_local` 为true
169    /// </details>
170    fn default() -> Self {
171        let app_name = env::var("CARGO_PKG_NAME");
172        if app_name.is_err() {
173            return Self::with_force_local("", true);
174        }
175        Self::new(&app_name.unwrap())
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use std::fs;
183
184    #[test]
185    fn test_app_data_new() {
186        let app_data = AppData::new("test_app");
187        assert_eq!(app_data.app_name, "test_app");
188        assert_eq!(app_data.force_local, false);
189    }
190
191    #[test]
192    fn test_app_data_with_force_local() {
193        let app_data = AppData::with_force_local("test_app", true);
194        assert_eq!(app_data.app_name, "test_app");
195        assert_eq!(app_data.force_local, true);
196    }
197
198    #[test]
199    fn test_app_data_debug() {
200        let app_data = AppData::new("test_app");
201        let debug_str = format!("{:?}", app_data);
202        assert!(debug_str.contains("test_app"));
203    }
204
205    #[test]
206    fn test_app_data_clone() {
207        let app_data = AppData::new("test_app");
208        let cloned = app_data.clone();
209        assert_eq!(app_data, cloned);
210    }
211
212    #[test]
213    fn test_app_data_partial_eq() {
214        let app_data1 = AppData::new("test_app");
215        let app_data2 = AppData::new("test_app");
216        let app_data3 = AppData::new("other_app");
217        assert_eq!(app_data1, app_data2);
218        assert_ne!(app_data1, app_data3);
219    }
220
221    #[test]
222    fn test_ensure_data_dir_force_local() {
223        let app_data = AppData::with_force_local("test_app", true);
224        let result = app_data.ensure_data_dir();
225        assert!(result.is_ok());
226        let data_dir = result.unwrap();
227        assert!(data_dir.exists());
228        assert!(data_dir.is_dir());
229        assert!(data_dir.ends_with("data"));
230
231        // 清理
232        let _ = fs::remove_dir_all(&data_dir);
233    }
234
235    #[test]
236    fn test_get_file_path() {
237        let app_data = AppData::with_force_local("test_app", true);
238        let result = app_data.get_file_path("test.txt");
239        assert!(result.is_ok());
240        let file_path = result.unwrap();
241        assert!(file_path.ends_with("test.txt"));
242
243        // 清理
244        if let Ok(data_dir) = app_data.ensure_data_dir() {
245            let _ = fs::remove_dir_all(&data_dir);
246        }
247    }
248
249    #[test]
250    fn test_app_data_error_display() {
251        let error = AppDataError::EnvVarNotFound("TEST_VAR".to_string());
252        let error_str = format!("{}", error);
253        assert!(error_str.contains("TEST_VAR"));
254    }
255
256    #[test]
257    fn test_app_data_error_from_io_error() {
258        let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "test");
259        let app_error: AppDataError = io_error.into();
260        match app_error {
261            AppDataError::IoError(_) => {}
262            _ => panic!("Expected IoError"),
263        }
264    }
265}