1use std::{
2 env::{self, current_dir, var},
3 fmt, fs,
4 path::PathBuf,
5};
6
7#[derive(Debug, Clone)]
9pub enum AppDataError {
10 EnvVarNotFound(String),
12 IoError(String),
14 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#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct AppData {
80 pub app_name: String,
85 pub force_local: bool,
90}
91
92impl 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 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 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 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 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 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}