use crate::error::{IoOperation, StorageError, StorageResult};
use std::fs;
use std::path::{Path, PathBuf};
use tracing::{debug, info};
const STORAGE_PATH_MARKER: &str = ".ricecoder_storage_path";
pub struct RelocationService;
impl RelocationService {
pub fn relocate(from: &Path, to: &Path) -> StorageResult<()> {
debug!(
"Starting relocation from {} to {}",
from.display(),
to.display()
);
if !from.exists() {
return Err(StorageError::relocation_error(
from.to_path_buf(),
to.to_path_buf(),
"Source directory does not exist",
));
}
if to.exists() {
if to.is_dir() {
let entries = fs::read_dir(to)
.map_err(|e| StorageError::io_error(to.to_path_buf(), IoOperation::Read, e))?;
if entries.count() > 0 {
return Err(StorageError::relocation_error(
from.to_path_buf(),
to.to_path_buf(),
"Target directory is not empty",
));
}
} else {
return Err(StorageError::relocation_error(
from.to_path_buf(),
to.to_path_buf(),
"Target path exists and is not a directory",
));
}
}
if let Some(parent) = to.parent() {
if !parent.exists() {
fs::create_dir_all(parent).map_err(|e| {
StorageError::directory_creation_failed(parent.to_path_buf(), e)
})?;
}
}
Self::copy_dir_recursive(from, to)?;
let source_count = Self::count_files(from)?;
let target_count = Self::count_files(to)?;
if source_count != target_count {
let _ = fs::remove_dir_all(to);
return Err(StorageError::relocation_error(
from.to_path_buf(),
to.to_path_buf(),
format!(
"Data integrity check failed: {} files in source, {} in target",
source_count, target_count
),
));
}
Self::update_storage_path_marker(to)?;
info!(
"Successfully relocated storage from {} to {}",
from.display(),
to.display()
);
Ok(())
}
pub fn get_stored_path(marker_dir: &Path) -> StorageResult<Option<PathBuf>> {
let marker_path = marker_dir.join(STORAGE_PATH_MARKER);
if !marker_path.exists() {
return Ok(None);
}
let content = fs::read_to_string(&marker_path)
.map_err(|e| StorageError::io_error(marker_path.clone(), IoOperation::Read, e))?;
let path = PathBuf::from(content.trim());
debug!("Read stored storage path: {}", path.display());
Ok(Some(path))
}
fn copy_dir_recursive(src: &Path, dst: &Path) -> StorageResult<()> {
fs::create_dir_all(dst)
.map_err(|e| StorageError::directory_creation_failed(dst.to_path_buf(), e))?;
for entry in fs::read_dir(src)
.map_err(|e| StorageError::io_error(src.to_path_buf(), IoOperation::Read, e))?
{
let entry = entry
.map_err(|e| StorageError::io_error(src.to_path_buf(), IoOperation::Read, e))?;
let path = entry.path();
let file_name = entry.file_name();
let dest_path = dst.join(&file_name);
if path.is_dir() {
Self::copy_dir_recursive(&path, &dest_path)?;
} else {
fs::copy(&path, &dest_path)
.map_err(|e| StorageError::io_error(path.clone(), IoOperation::Read, e))?;
}
}
Ok(())
}
fn count_files(dir: &Path) -> StorageResult<usize> {
let mut count = 0;
for entry in fs::read_dir(dir)
.map_err(|e| StorageError::io_error(dir.to_path_buf(), IoOperation::Read, e))?
{
let entry = entry
.map_err(|e| StorageError::io_error(dir.to_path_buf(), IoOperation::Read, e))?;
let path = entry.path();
if path.is_dir() {
count += Self::count_files(&path)?;
} else {
count += 1;
}
}
Ok(count)
}
fn update_storage_path_marker(storage_path: &Path) -> StorageResult<()> {
let home = dirs::home_dir().ok_or_else(|| {
StorageError::path_resolution_error("Could not determine home directory")
})?;
let marker_path = home.join(STORAGE_PATH_MARKER);
let path_str = storage_path.to_str().ok_or_else(|| {
StorageError::path_resolution_error("Could not convert path to string")
})?;
fs::write(&marker_path, path_str)
.map_err(|e| StorageError::io_error(marker_path.clone(), IoOperation::Write, e))?;
debug!("Updated storage path marker: {}", marker_path.display());
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_relocation_success() -> StorageResult<()> {
let source_dir = TempDir::new().unwrap();
let target_dir = TempDir::new().unwrap();
fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
fs::write(source_dir.path().join("file2.txt"), "content2").unwrap();
let source_path = source_dir.path().to_path_buf();
let target_path = target_dir.path().join("new_storage");
RelocationService::relocate(&source_path, &target_path)?;
assert!(target_path.join("file1.txt").exists());
assert!(target_path.join("file2.txt").exists());
let content1 = fs::read_to_string(target_path.join("file1.txt")).unwrap();
assert_eq!(content1, "content1");
Ok(())
}
#[test]
fn test_relocation_with_subdirs() -> StorageResult<()> {
let source_dir = TempDir::new().unwrap();
let target_dir = TempDir::new().unwrap();
fs::create_dir(source_dir.path().join("subdir")).unwrap();
fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
fs::write(source_dir.path().join("subdir/file2.txt"), "content2").unwrap();
let source_path = source_dir.path().to_path_buf();
let target_path = target_dir.path().join("new_storage");
RelocationService::relocate(&source_path, &target_path)?;
assert!(target_path.join("file1.txt").exists());
assert!(target_path.join("subdir/file2.txt").exists());
Ok(())
}
#[test]
fn test_relocation_source_not_exists() {
let source_path = PathBuf::from("/nonexistent/source");
let target_path = PathBuf::from("/nonexistent/target");
let result = RelocationService::relocate(&source_path, &target_path);
assert!(result.is_err());
}
#[test]
fn test_relocation_target_not_empty() -> StorageResult<()> {
let source_dir = TempDir::new().unwrap();
let target_dir = TempDir::new().unwrap();
fs::write(source_dir.path().join("file1.txt"), "content1").unwrap();
fs::write(target_dir.path().join("existing.txt"), "existing").unwrap();
let source_path = source_dir.path().to_path_buf();
let target_path = target_dir.path().to_path_buf();
let result = RelocationService::relocate(&source_path, &target_path);
assert!(result.is_err());
Ok(())
}
#[test]
fn test_count_files() -> StorageResult<()> {
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("file1.txt"), "content1").unwrap();
fs::write(temp_dir.path().join("file2.txt"), "content2").unwrap();
fs::create_dir(temp_dir.path().join("subdir")).unwrap();
fs::write(temp_dir.path().join("subdir/file3.txt"), "content3").unwrap();
let count = RelocationService::count_files(temp_dir.path())?;
assert_eq!(count, 3);
Ok(())
}
}