ricecoder_storage/
relocation.rs1use crate::error::{IoOperation, StorageError, StorageResult};
7use std::fs;
8use std::path::{Path, PathBuf};
9use tracing::{debug, info};
10
11const STORAGE_PATH_MARKER: &str = ".ricecoder_storage_path";
13
14pub struct RelocationService;
16
17impl RelocationService {
18 pub fn relocate(from: &Path, to: &Path) -> StorageResult<()> {
29 debug!(
30 "Starting relocation from {} to {}",
31 from.display(),
32 to.display()
33 );
34
35 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 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 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 Self::copy_dir_recursive(from, to)?;
77
78 let source_count = Self::count_files(from)?;
80 let target_count = Self::count_files(to)?;
81
82 if source_count != target_count {
83 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 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 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 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 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 fn update_storage_path_marker(storage_path: &Path) -> StorageResult<()> {
182 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 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 RelocationService::relocate(&source_path, &target_path)?;
221
222 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 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 RelocationService::relocate(&source_path, &target_path)?;
247
248 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 fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
271
272 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 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}