ricecoder_storage/
relocation.rs

1//! Storage relocation functionality
2//!
3//! Provides functionality to move global storage to a new location
4//! and update configuration pointers.
5
6use crate::error::{IoOperation, StorageError, StorageResult};
7use std::fs;
8use std::path::{Path, PathBuf};
9use tracing::{debug, info};
10
11/// Marker file that stores the current global storage path
12const STORAGE_PATH_MARKER: &str = ".ricecoder_storage_path";
13
14/// Relocation service for moving global storage
15pub struct RelocationService;
16
17impl RelocationService {
18    /// Relocate storage from one location to another
19    ///
20    /// # Arguments
21    ///
22    /// * `from` - Current storage location
23    /// * `to` - New storage location
24    ///
25    /// # Errors
26    ///
27    /// Returns error if relocation fails
28    pub fn relocate(from: &Path, to: &Path) -> StorageResult<()> {
29        debug!(
30            "Starting relocation from {} to {}",
31            from.display(),
32            to.display()
33        );
34
35        // Validate source exists
36        if !from.exists() {
37            return Err(StorageError::relocation_error(
38                from.to_path_buf(),
39                to.to_path_buf(),
40                "Source directory does not exist",
41            ));
42        }
43
44        // Validate target doesn't exist or is empty
45        if to.exists() {
46            if to.is_dir() {
47                let entries = fs::read_dir(to)
48                    .map_err(|e| StorageError::io_error(to.to_path_buf(), IoOperation::Read, e))?;
49
50                if entries.count() > 0 {
51                    return Err(StorageError::relocation_error(
52                        from.to_path_buf(),
53                        to.to_path_buf(),
54                        "Target directory is not empty",
55                    ));
56                }
57            } else {
58                return Err(StorageError::relocation_error(
59                    from.to_path_buf(),
60                    to.to_path_buf(),
61                    "Target path exists and is not a directory",
62                ));
63            }
64        }
65
66        // Create target parent directory if needed
67        if let Some(parent) = to.parent() {
68            if !parent.exists() {
69                fs::create_dir_all(parent).map_err(|e| {
70                    StorageError::directory_creation_failed(parent.to_path_buf(), e)
71                })?;
72            }
73        }
74
75        // Copy all data from source to target
76        Self::copy_dir_recursive(from, to)?;
77
78        // Verify data integrity by checking file count
79        let source_count = Self::count_files(from)?;
80        let target_count = Self::count_files(to)?;
81
82        if source_count != target_count {
83            // Cleanup target on failure
84            let _ = fs::remove_dir_all(to);
85            return Err(StorageError::relocation_error(
86                from.to_path_buf(),
87                to.to_path_buf(),
88                format!(
89                    "Data integrity check failed: {} files in source, {} in target",
90                    source_count, target_count
91                ),
92            ));
93        }
94
95        // Update configuration pointer
96        Self::update_storage_path_marker(to)?;
97
98        info!(
99            "Successfully relocated storage from {} to {}",
100            from.display(),
101            to.display()
102        );
103
104        Ok(())
105    }
106
107    /// Get the stored storage path from marker file
108    ///
109    /// # Arguments
110    ///
111    /// * `marker_dir` - Directory containing the marker file
112    ///
113    /// # Returns
114    ///
115    /// Returns the stored path if marker exists, None otherwise
116    pub fn get_stored_path(marker_dir: &Path) -> StorageResult<Option<PathBuf>> {
117        let marker_path = marker_dir.join(STORAGE_PATH_MARKER);
118
119        if !marker_path.exists() {
120            return Ok(None);
121        }
122
123        let content = fs::read_to_string(&marker_path)
124            .map_err(|e| StorageError::io_error(marker_path.clone(), IoOperation::Read, e))?;
125
126        let path = PathBuf::from(content.trim());
127        debug!("Read stored storage path: {}", path.display());
128
129        Ok(Some(path))
130    }
131
132    /// Copy directory recursively
133    fn copy_dir_recursive(src: &Path, dst: &Path) -> StorageResult<()> {
134        fs::create_dir_all(dst)
135            .map_err(|e| StorageError::directory_creation_failed(dst.to_path_buf(), e))?;
136
137        for entry in fs::read_dir(src)
138            .map_err(|e| StorageError::io_error(src.to_path_buf(), IoOperation::Read, e))?
139        {
140            let entry = entry
141                .map_err(|e| StorageError::io_error(src.to_path_buf(), IoOperation::Read, e))?;
142
143            let path = entry.path();
144            let file_name = entry.file_name();
145            let dest_path = dst.join(&file_name);
146
147            if path.is_dir() {
148                Self::copy_dir_recursive(&path, &dest_path)?;
149            } else {
150                fs::copy(&path, &dest_path)
151                    .map_err(|e| StorageError::io_error(path.clone(), IoOperation::Read, e))?;
152            }
153        }
154
155        Ok(())
156    }
157
158    /// Count files in directory recursively
159    fn count_files(dir: &Path) -> StorageResult<usize> {
160        let mut count = 0;
161
162        for entry in fs::read_dir(dir)
163            .map_err(|e| StorageError::io_error(dir.to_path_buf(), IoOperation::Read, e))?
164        {
165            let entry = entry
166                .map_err(|e| StorageError::io_error(dir.to_path_buf(), IoOperation::Read, e))?;
167
168            let path = entry.path();
169
170            if path.is_dir() {
171                count += Self::count_files(&path)?;
172            } else {
173                count += 1;
174            }
175        }
176
177        Ok(count)
178    }
179
180    /// Update the storage path marker file
181    fn update_storage_path_marker(storage_path: &Path) -> StorageResult<()> {
182        // Get the home directory to store the marker
183        let home = dirs::home_dir().ok_or_else(|| {
184            StorageError::path_resolution_error("Could not determine home directory")
185        })?;
186
187        let marker_path = home.join(STORAGE_PATH_MARKER);
188
189        let path_str = storage_path.to_str().ok_or_else(|| {
190            StorageError::path_resolution_error("Could not convert path to string")
191        })?;
192
193        fs::write(&marker_path, path_str)
194            .map_err(|e| StorageError::io_error(marker_path.clone(), IoOperation::Write, e))?;
195
196        debug!("Updated storage path marker: {}", marker_path.display());
197
198        Ok(())
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use tempfile::TempDir;
206
207    #[test]
208    fn test_relocation_success() -> StorageResult<()> {
209        let source_dir = TempDir::new().unwrap();
210        let target_dir = TempDir::new().unwrap();
211
212        // Create some test files in source
213        fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
214        fs::write(source_dir.path().join("file2.txt"), "content2").unwrap();
215
216        let source_path = source_dir.path().to_path_buf();
217        let target_path = target_dir.path().join("new_storage");
218
219        // Perform relocation
220        RelocationService::relocate(&source_path, &target_path)?;
221
222        // Verify files were copied
223        assert!(target_path.join("file1.txt").exists());
224        assert!(target_path.join("file2.txt").exists());
225
226        let content1 = fs::read_to_string(target_path.join("file1.txt")).unwrap();
227        assert_eq!(content1, "content1");
228
229        Ok(())
230    }
231
232    #[test]
233    fn test_relocation_with_subdirs() -> StorageResult<()> {
234        let source_dir = TempDir::new().unwrap();
235        let target_dir = TempDir::new().unwrap();
236
237        // Create nested structure
238        fs::create_dir(source_dir.path().join("subdir")).unwrap();
239        fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
240        fs::write(source_dir.path().join("subdir/file2.txt"), "content2").unwrap();
241
242        let source_path = source_dir.path().to_path_buf();
243        let target_path = target_dir.path().join("new_storage");
244
245        // Perform relocation
246        RelocationService::relocate(&source_path, &target_path)?;
247
248        // Verify structure was copied
249        assert!(target_path.join("file1.txt").exists());
250        assert!(target_path.join("subdir/file2.txt").exists());
251
252        Ok(())
253    }
254
255    #[test]
256    fn test_relocation_source_not_exists() {
257        let source_path = PathBuf::from("/nonexistent/source");
258        let target_path = PathBuf::from("/nonexistent/target");
259
260        let result = RelocationService::relocate(&source_path, &target_path);
261        assert!(result.is_err());
262    }
263
264    #[test]
265    fn test_relocation_target_not_empty() -> StorageResult<()> {
266        let source_dir = TempDir::new().unwrap();
267        let target_dir = TempDir::new().unwrap();
268
269        // Create file in source
270        fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
271
272        // Create file in target to make it non-empty
273        fs::write(target_dir.path().join("existing.txt"), "existing").unwrap();
274
275        let source_path = source_dir.path().to_path_buf();
276        let target_path = target_dir.path().to_path_buf();
277
278        let result = RelocationService::relocate(&source_path, &target_path);
279        assert!(result.is_err());
280
281        Ok(())
282    }
283
284    #[test]
285    fn test_count_files() -> StorageResult<()> {
286        let temp_dir = TempDir::new().unwrap();
287
288        // Create test structure
289        fs::write(temp_dir.path().join("file1.txt"), "content1").unwrap();
290        fs::write(temp_dir.path().join("file2.txt"), "content2").unwrap();
291        fs::create_dir(temp_dir.path().join("subdir")).unwrap();
292        fs::write(temp_dir.path().join("subdir/file3.txt"), "content3").unwrap();
293
294        let count = RelocationService::count_files(temp_dir.path())?;
295        assert_eq!(count, 3);
296
297        Ok(())
298    }
299}