ricecoder_storage/
offline.rs

1//! Offline mode handling for storage
2//!
3//! Provides functionality to detect when storage is unavailable and operate
4//! in read-only mode with cached data.
5
6use crate::error::{StorageError, StorageResult};
7use crate::types::StorageState;
8use std::path::Path;
9use std::time::{SystemTime, UNIX_EPOCH};
10use tracing::{debug, warn};
11
12/// Offline mode handler
13pub struct OfflineModeHandler;
14
15impl OfflineModeHandler {
16    /// Check if storage is available
17    ///
18    /// # Arguments
19    ///
20    /// * `storage_path` - Path to storage directory
21    ///
22    /// # Returns
23    ///
24    /// Returns the storage state (Available, Unavailable, or ReadOnly)
25    pub fn check_storage_availability(storage_path: &Path) -> StorageState {
26        // Check if path exists
27        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        // Check if path is accessible (try to read directory)
38        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    /// Check if storage is on external or network drive
57    ///
58    /// # Arguments
59    ///
60    /// * `storage_path` - Path to storage directory
61    ///
62    /// # Returns
63    ///
64    /// Returns true if storage appears to be on external/network storage
65    pub fn is_external_storage(storage_path: &Path) -> bool {
66        let path_str = storage_path.to_string_lossy();
67
68        // Check for common network/external indicators
69        #[cfg(target_os = "windows")]
70        {
71            // Check for UNC paths (network drives)
72            if path_str.starts_with("\\\\") {
73                return true;
74            }
75            // Check for mapped drives (typically Z:, Y:, etc.)
76            if let Some(drive) = path_str.chars().next() {
77                if drive.is_alphabetic() {
78                    let drive_letter = drive.to_ascii_uppercase();
79                    // Assume drives beyond D: might be external/network
80                    if drive_letter > 'D' {
81                        return true;
82                    }
83                }
84            }
85        }
86
87        #[cfg(target_os = "macos")]
88        {
89            // Check for mounted volumes
90            if path_str.starts_with("/Volumes/") {
91                return true;
92            }
93        }
94
95        #[cfg(target_os = "linux")]
96        {
97            // Check for mounted filesystems
98            if path_str.starts_with("/mnt/") || path_str.starts_with("/media/") {
99                return true;
100            }
101        }
102
103        false
104    }
105
106    /// Transition to offline mode
107    ///
108    /// # Arguments
109    ///
110    /// * `storage_path` - Path to storage directory
111    /// * `cache_available` - Whether cached data is available
112    ///
113    /// # Returns
114    ///
115    /// Returns the new storage state
116    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    /// Check if we should retry storage access
141    ///
142    /// # Arguments
143    ///
144    /// * `storage_path` - Path to storage directory
145    ///
146    /// # Returns
147    ///
148    /// Returns true if storage is now available
149    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    /// Log offline mode warning
163    ///
164    /// # Arguments
165    ///
166    /// * `storage_path` - Path to storage directory
167    /// * `reason` - Reason for offline mode
168    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    /// Validate that we can operate in offline mode
177    ///
178    /// # Arguments
179    ///
180    /// * `cache_available` - Whether cached data is available
181    ///
182    /// # Returns
183    ///
184    /// Returns error if offline mode cannot be used
185    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                // Expected
218            }
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                // Expected
231            }
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                // Expected
244            }
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        // This test is platform-specific
280        #[cfg(target_os = "windows")]
281        {
282            // UNC paths are external
283            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            // /Volumes paths are external
290            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            // /mnt and /media paths are external
297            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}