ricecoder_storage/
offline.rs1use crate::error::{StorageError, StorageResult};
7use crate::types::StorageState;
8use std::path::Path;
9use std::time::{SystemTime, UNIX_EPOCH};
10use tracing::{debug, warn};
11
12pub struct OfflineModeHandler;
14
15impl OfflineModeHandler {
16 pub fn check_storage_availability(storage_path: &Path) -> StorageState {
26 if !storage_path.exists() {
28 warn!(
29 "Storage unavailable: path does not exist: {}",
30 storage_path.display()
31 );
32 return StorageState::Unavailable {
33 reason: "Storage path does not exist".to_string(),
34 };
35 }
36
37 match std::fs::read_dir(storage_path) {
39 Ok(_) => {
40 debug!("Storage is available: {}", storage_path.display());
41 StorageState::Available
42 }
43 Err(e) => {
44 warn!(
45 "Storage unavailable: cannot read directory {}: {}",
46 storage_path.display(),
47 e
48 );
49 StorageState::Unavailable {
50 reason: format!("Cannot read directory: {}", e),
51 }
52 }
53 }
54 }
55
56 pub fn is_external_storage(storage_path: &Path) -> bool {
66 let path_str = storage_path.to_string_lossy();
67
68 #[cfg(target_os = "windows")]
70 {
71 if path_str.starts_with("\\\\") {
73 return true;
74 }
75 if let Some(drive) = path_str.chars().next() {
77 if drive.is_alphabetic() {
78 let drive_letter = drive.to_ascii_uppercase();
79 if drive_letter > 'D' {
81 return true;
82 }
83 }
84 }
85 }
86
87 #[cfg(target_os = "macos")]
88 {
89 if path_str.starts_with("/Volumes/") {
91 return true;
92 }
93 }
94
95 #[cfg(target_os = "linux")]
96 {
97 if path_str.starts_with("/mnt/") || path_str.starts_with("/media/") {
99 return true;
100 }
101 }
102
103 false
104 }
105
106 pub fn enter_offline_mode(storage_path: &Path, cache_available: bool) -> StorageState {
117 let cached_at = SystemTime::now()
118 .duration_since(UNIX_EPOCH)
119 .unwrap_or_default()
120 .as_secs()
121 .to_string();
122
123 if cache_available {
124 warn!(
125 "Entering offline mode for storage: {}. Using cached data.",
126 storage_path.display()
127 );
128 StorageState::ReadOnly { cached_at }
129 } else {
130 warn!(
131 "Entering offline mode for storage: {}. No cached data available.",
132 storage_path.display()
133 );
134 StorageState::Unavailable {
135 reason: "Storage unavailable and no cached data available".to_string(),
136 }
137 }
138 }
139
140 pub fn retry_storage_access(storage_path: &Path) -> bool {
150 match Self::check_storage_availability(storage_path) {
151 StorageState::Available => {
152 debug!("Storage is now available: {}", storage_path.display());
153 true
154 }
155 _ => {
156 debug!("Storage is still unavailable: {}", storage_path.display());
157 false
158 }
159 }
160 }
161
162 pub fn log_offline_warning(storage_path: &Path, reason: &str) {
169 warn!(
170 "Storage offline mode activated for {}: {}",
171 storage_path.display(),
172 reason
173 );
174 }
175
176 pub fn validate_offline_mode(cache_available: bool) -> StorageResult<()> {
186 if !cache_available {
187 return Err(StorageError::internal(
188 "Cannot enter offline mode: no cached data available",
189 ));
190 }
191
192 debug!("Offline mode validation passed");
193 Ok(())
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use tempfile::TempDir;
201
202 #[test]
203 fn test_check_storage_availability_exists() {
204 let temp_dir = TempDir::new().unwrap();
205 let state = OfflineModeHandler::check_storage_availability(temp_dir.path());
206
207 assert_eq!(state, StorageState::Available);
208 }
209
210 #[test]
211 fn test_check_storage_availability_not_exists() {
212 let path = std::path::PathBuf::from("/nonexistent/path/that/does/not/exist");
213 let state = OfflineModeHandler::check_storage_availability(&path);
214
215 match state {
216 StorageState::Unavailable { .. } => {
217 }
219 _ => panic!("Expected Unavailable state"),
220 }
221 }
222
223 #[test]
224 fn test_enter_offline_mode_with_cache() {
225 let temp_dir = TempDir::new().unwrap();
226 let state = OfflineModeHandler::enter_offline_mode(temp_dir.path(), true);
227
228 match state {
229 StorageState::ReadOnly { .. } => {
230 }
232 _ => panic!("Expected ReadOnly state"),
233 }
234 }
235
236 #[test]
237 fn test_enter_offline_mode_without_cache() {
238 let temp_dir = TempDir::new().unwrap();
239 let state = OfflineModeHandler::enter_offline_mode(temp_dir.path(), false);
240
241 match state {
242 StorageState::Unavailable { .. } => {
243 }
245 _ => panic!("Expected Unavailable state"),
246 }
247 }
248
249 #[test]
250 fn test_retry_storage_access_available() {
251 let temp_dir = TempDir::new().unwrap();
252 let result = OfflineModeHandler::retry_storage_access(temp_dir.path());
253
254 assert!(result);
255 }
256
257 #[test]
258 fn test_retry_storage_access_unavailable() {
259 let path = std::path::PathBuf::from("/nonexistent/path");
260 let result = OfflineModeHandler::retry_storage_access(&path);
261
262 assert!(!result);
263 }
264
265 #[test]
266 fn test_validate_offline_mode_with_cache() {
267 let result = OfflineModeHandler::validate_offline_mode(true);
268 assert!(result.is_ok());
269 }
270
271 #[test]
272 fn test_validate_offline_mode_without_cache() {
273 let result = OfflineModeHandler::validate_offline_mode(false);
274 assert!(result.is_err());
275 }
276
277 #[test]
278 fn test_is_external_storage() {
279 #[cfg(target_os = "windows")]
281 {
282 let unc_path = std::path::PathBuf::from("\\\\server\\share");
284 assert!(OfflineModeHandler::is_external_storage(&unc_path));
285 }
286
287 #[cfg(target_os = "macos")]
288 {
289 let volume_path = std::path::PathBuf::from("/Volumes/ExternalDrive");
291 assert!(OfflineModeHandler::is_external_storage(&volume_path));
292 }
293
294 #[cfg(target_os = "linux")]
295 {
296 let mnt_path = std::path::PathBuf::from("/mnt/external");
298 assert!(OfflineModeHandler::is_external_storage(&mnt_path));
299
300 let media_path = std::path::PathBuf::from("/media/user/external");
301 assert!(OfflineModeHandler::is_external_storage(&media_path));
302 }
303 }
304}