use crate::error::{CpdError, Result};
use indicatif::{ProgressBar, ProgressStyle};
use std::fs;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
pub struct FileOperations {
verbose: bool,
}
impl FileOperations {
pub fn new(verbose: bool) -> Self {
Self { verbose }
}
pub fn copy_file(&self, from: &Path, to: &Path) -> Result<()> {
if let Some(parent) = to.parent() {
fs::create_dir_all(parent).map_err(|e| {
if self.verbose {
eprintln!("Failed to create directory {}: {}", parent.display(), e);
}
CpdError::Io(e)
})?;
}
fs::copy(from, to).map_err(|e| {
if self.verbose {
eprintln!("Failed to copy {} to {}: {}", from.display(), to.display(), e);
}
CpdError::FileCopyFailed {
from: from.display().to_string(),
to: to.display().to_string(),
}
})?;
if let Ok(metadata) = fs::metadata(from) {
if let Ok(modified) = metadata.modified() {
let _ = filetime::set_file_mtime(to, filetime::FileTime::from_system_time(modified));
}
}
if self.verbose {
println!("Copied: {} -> {}", from.display(), to.display());
}
Ok(())
}
pub fn copy_directory_contents(
&self,
from_dir: &Path,
to_dir: &Path,
filter: &dyn Fn(&Path) -> bool,
dry_run: bool,
) -> Result<CopyResult> {
let mut files_to_copy = Vec::new();
let mut _total_size = 0u64;
for entry in WalkDir::new(from_dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
{
let path = entry.path();
if filter(path) {
if let Ok(metadata) = entry.metadata() {
_total_size += metadata.len();
}
files_to_copy.push(path.to_path_buf());
}
}
let progress = if !dry_run && !files_to_copy.is_empty() {
let pb = ProgressBar::new(files_to_copy.len() as u64);
pb.set_style(
ProgressStyle::default_bar()
.template("[{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}")
.unwrap()
.progress_chars("##-"),
);
Some(pb)
} else {
None
};
let mut result = CopyResult {
files_copied: 0,
files_failed: 0,
bytes_copied: 0,
failed_files: Vec::new(),
};
for file_path in &files_to_copy {
let relative_path = file_path.strip_prefix(from_dir).unwrap();
let dest_path = to_dir.join(relative_path);
if let Some(pb) = &progress {
pb.set_message(format!("Copying {}", relative_path.display()));
}
if dry_run {
println!("Would copy: {} -> {}", file_path.display(), dest_path.display());
result.files_copied += 1;
} else {
match self.copy_file(file_path, &dest_path) {
Ok(_) => {
result.files_copied += 1;
if let Ok(metadata) = fs::metadata(file_path) {
result.bytes_copied += metadata.len();
}
}
Err(e) => {
result.files_failed += 1;
result.failed_files.push((file_path.clone(), e.to_string()));
if !e.is_recoverable() {
if let Some(pb) = &progress {
pb.finish_with_message("Deployment failed");
}
return Err(e);
}
}
}
}
if let Some(pb) = &progress {
pb.inc(1);
}
}
if let Some(pb) = &progress {
pb.finish_with_message("Deployment completed");
}
Ok(result)
}
pub fn create_backup(&self, source_dir: &Path, backup_dir: &Path) -> Result<()> {
if !source_dir.exists() {
return Ok(()); }
fs::create_dir_all(backup_dir).map_err(|_| CpdError::BackupDirectoryCreationFailed {
path: backup_dir.display().to_string(),
})?;
let result = self.copy_directory_contents(
source_dir,
backup_dir,
&|_| true, false,
)?;
if self.verbose {
println!(
"Backup completed: {} files, {} bytes",
result.files_copied,
format_bytes(result.bytes_copied)
);
}
Ok(())
}
#[allow(dead_code)]
pub fn clean_destination(&self, source_dir: &Path, dest_dir: &Path, filter: &dyn Fn(&Path) -> bool) -> Result<()> {
if !dest_dir.exists() {
return Ok(());
}
let mut files_to_remove = Vec::new();
for entry in WalkDir::new(dest_dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
{
let dest_path = entry.path();
let relative_path = dest_path.strip_prefix(dest_dir).unwrap();
let source_path = source_dir.join(relative_path);
if !source_path.exists() || !filter(&source_path) {
files_to_remove.push(dest_path.to_path_buf());
}
}
for file_path in files_to_remove {
if let Err(e) = fs::remove_file(&file_path) {
if self.verbose {
eprintln!("Failed to remove {}: {}", file_path.display(), e);
}
} else if self.verbose {
println!("Removed: {}", file_path.display());
}
}
Ok(())
}
}
#[derive(Debug)]
pub struct CopyResult {
pub files_copied: usize,
pub files_failed: usize,
pub bytes_copied: u64,
pub failed_files: Vec<(PathBuf, String)>,
}
impl CopyResult {
#[allow(dead_code)]
pub fn is_success(&self) -> bool {
self.files_failed == 0
}
pub fn summary(&self) -> String {
if self.files_failed == 0 {
format!(
"Successfully copied {} files ({})",
self.files_copied,
format_bytes(self.bytes_copied)
)
} else {
format!(
"Copied {} files, {} failed ({})",
self.files_copied,
self.files_failed,
format_bytes(self.bytes_copied)
)
}
}
}
fn format_bytes(bytes: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
let mut size = bytes as f64;
let mut unit_index = 0;
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
size /= 1024.0;
unit_index += 1;
}
if unit_index == 0 {
format!("{} {}", bytes, UNITS[unit_index])
} else {
format!("{:.1} {}", size, UNITS[unit_index])
}
}
extern crate filetime;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_bytes() {
assert_eq!(format_bytes(0), "0 B");
assert_eq!(format_bytes(512), "512 B");
assert_eq!(format_bytes(1024), "1.0 KB");
assert_eq!(format_bytes(1536), "1.5 KB");
assert_eq!(format_bytes(1048576), "1.0 MB");
assert_eq!(format_bytes(1073741824), "1.0 GB");
}
}