use crate::config::Config;
use crate::error::QbakError;
use crate::naming::{generate_backup_name, resolve_collision};
use crate::progress::{create_progress_bar, BackupProgress};
use crate::utils::{
calculate_size, copy_permissions, copy_timestamps, format_size, is_hidden, validate_source,
};
use crate::Result;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
#[derive(Debug)]
pub struct BackupResult {
pub source_path: PathBuf,
pub backup_path: PathBuf,
pub files_processed: usize,
pub total_size: u64,
pub duration: Duration,
}
impl BackupResult {
pub fn new(source_path: PathBuf, backup_path: PathBuf) -> Self {
Self {
source_path,
backup_path,
files_processed: 0,
total_size: 0,
duration: Duration::from_secs(0),
}
}
pub fn summary(&self) -> String {
if self.files_processed == 1 {
format!(
"Created backup: {} ({})",
self.backup_path.display(),
format_size(self.total_size)
)
} else {
format!(
"Created backup: {} ({} files, {})",
self.backup_path.display(),
self.files_processed,
format_size(self.total_size)
)
}
}
}
pub fn backup_file(source: &Path, config: &Config) -> Result<BackupResult> {
let start_time = Instant::now();
validate_source(source)?;
let backup_path = generate_backup_name(source, config)?;
let final_backup_path = resolve_collision(&backup_path)?;
let _operation_guard = crate::signal::create_backup_guard(final_backup_path.clone());
let file_size = calculate_size(source)?;
let temp_path = create_temp_backup_path(&final_backup_path)?;
copy_file_with_interrupt_check(source, &temp_path)?;
if config.preserve_permissions {
copy_permissions(source, &temp_path)?;
copy_timestamps(source, &temp_path)?;
}
fs::rename(&temp_path, &final_backup_path)?;
let duration = start_time.elapsed();
let result = BackupResult {
source_path: source.to_path_buf(),
backup_path: final_backup_path,
files_processed: 1,
total_size: file_size,
duration,
};
_operation_guard.complete();
Ok(result)
}
pub fn backup_directory(source: &Path, config: &Config, verbose: bool) -> Result<BackupResult> {
let start_time = Instant::now();
validate_source(source)?;
if !source.is_dir() {
return Err(QbakError::validation("Source is not a directory"));
}
let backup_path = generate_backup_name(source, config)?;
let final_backup_path = resolve_collision(&backup_path)?;
let _operation_guard = crate::signal::create_backup_guard(final_backup_path.clone());
fs::create_dir_all(&final_backup_path)?;
let mut result = BackupResult::new(source.to_path_buf(), final_backup_path.clone());
let total_files = count_files_recursive(source, config)?;
let show_progress = verbose;
if show_progress {
println!("Backing up directory with {total_files} files...");
}
copy_directory_contents(
source,
&final_backup_path,
config,
&mut result,
show_progress,
)?;
if config.preserve_permissions {
copy_permissions(source, &final_backup_path)?;
copy_timestamps(source, &final_backup_path)?;
}
if show_progress {
println!(
"Directory backup completed: {} files processed",
result.files_processed
);
}
result.duration = start_time.elapsed();
_operation_guard.complete();
Ok(result)
}
fn copy_directory_contents(
source_dir: &Path,
backup_dir: &Path,
config: &Config,
result: &mut BackupResult,
show_progress: bool,
) -> Result<()> {
for entry in fs::read_dir(source_dir)? {
if crate::signal::is_interrupted() {
return Err(QbakError::Interrupted);
}
let entry = entry?;
let source_path = entry.path();
let file_name = entry.file_name();
if !config.include_hidden && is_hidden(&source_path) {
continue;
}
let backup_path = backup_dir.join(&file_name);
let metadata = entry.metadata()?;
if metadata.is_file() {
copy_file_to_backup(&source_path, &backup_path, config, result)?;
if show_progress && result.files_processed % 10 == 0 {
eprint!(".");
use std::io::{self, Write};
io::stderr().flush().unwrap_or(());
}
} else if metadata.is_dir() {
fs::create_dir_all(&backup_path)?;
copy_directory_contents(&source_path, &backup_path, config, result, show_progress)?;
if config.preserve_permissions {
copy_permissions(&source_path, &backup_path)?;
copy_timestamps(&source_path, &backup_path)?;
}
} else if metadata.file_type().is_symlink() {
handle_symlink(&source_path, &backup_path, config, result, show_progress)?;
}
}
Ok(())
}
fn copy_file_to_backup(
source: &Path,
backup: &Path,
config: &Config,
result: &mut BackupResult,
) -> Result<()> {
let temp_path = create_temp_backup_path(backup)?;
copy_file_with_interrupt_check(source, &temp_path)?;
if config.preserve_permissions {
copy_permissions(source, &temp_path)?;
copy_timestamps(source, &temp_path)?;
}
fs::rename(&temp_path, backup)?;
let file_size = fs::metadata(source)?.len();
result.files_processed += 1;
result.total_size += file_size;
Ok(())
}
fn copy_file_with_interrupt_check(source: &Path, dest: &Path) -> Result<()> {
use std::io::{Read, Write};
let mut source_file = fs::File::open(source)?;
let mut dest_file = fs::File::create(dest)?;
let mut buffer = vec![0u8; 64 * 1024];
loop {
if crate::signal::is_interrupted() {
let _ = fs::remove_file(dest);
return Err(QbakError::Interrupted);
}
let bytes_read = source_file.read(&mut buffer)?;
if bytes_read == 0 {
break; }
dest_file.write_all(&buffer[..bytes_read])?;
}
dest_file.flush()?;
Ok(())
}
fn handle_symlink(
source: &Path,
backup: &Path,
config: &Config,
result: &mut BackupResult,
show_progress: bool,
) -> Result<()> {
if config.follow_symlinks {
let target = fs::read_link(source)?;
let resolved_target = if target.is_absolute() {
target
} else {
source.parent().unwrap_or(Path::new(".")).join(target)
};
if resolved_target.exists() {
let metadata = fs::metadata(&resolved_target)?;
if metadata.is_file() {
copy_file_to_backup(&resolved_target, backup, config, result)?;
} else if metadata.is_dir() {
fs::create_dir_all(backup)?;
copy_directory_contents(&resolved_target, backup, config, result, show_progress)?;
}
}
} else {
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let target = fs::read_link(source)?;
symlink(target, backup)?;
}
#[cfg(not(unix))]
{
let target = fs::read_link(source)?;
let resolved_target = if target.is_absolute() {
target
} else {
source.parent().unwrap_or(Path::new(".")).join(target)
};
if resolved_target.exists() && resolved_target.is_file() {
copy_file_to_backup(&resolved_target, backup, config, result)?;
}
}
}
Ok(())
}
fn create_temp_backup_path(backup_path: &Path) -> Result<PathBuf> {
let parent = backup_path.parent().unwrap_or(Path::new("."));
let filename = backup_path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| QbakError::validation("Invalid backup filename"))?;
let random_suffix = crate::utils::generate_secure_random_string(16);
let temp_name = format!(".qbak_temp_{random_suffix}_{filename}");
Ok(parent.join(temp_name))
}
fn count_files_recursive(dir: &Path, config: &Config) -> Result<usize> {
let mut count = 0;
if !dir.is_dir() {
return Ok(1); }
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if !config.include_hidden && is_hidden(&path) {
continue;
}
let metadata = entry.metadata()?;
if metadata.is_file() {
count += 1;
} else if metadata.is_dir() {
count += count_files_recursive(&path, config)?;
} else if metadata.file_type().is_symlink() && config.follow_symlinks {
let target = fs::read_link(&path)?;
let resolved_target = if target.is_absolute() {
target
} else {
path.parent().unwrap_or(Path::new(".")).join(target)
};
if resolved_target.exists() {
let target_metadata = fs::metadata(&resolved_target)?;
if target_metadata.is_file() {
count += 1;
} else if target_metadata.is_dir() {
count += count_files_recursive(&resolved_target, config)?;
}
}
}
}
Ok(count)
}
pub fn cleanup_temp_files(dir: &Path) -> Result<()> {
if !dir.is_dir() {
return Ok(());
}
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if let Some(filename) = path.file_name().and_then(|name| name.to_str()) {
if filename.starts_with(".qbak_temp_") {
let _ = fs::remove_file(&path);
}
}
}
Ok(())
}
pub fn backup_directory_with_progress(
source: &Path,
config: &Config,
force_progress: bool,
quiet: bool,
) -> Result<BackupResult> {
let start_time = Instant::now();
validate_source(source)?;
let backup_path = generate_backup_name(source, config)?;
let final_backup_path = resolve_collision(&backup_path)?;
let _operation_guard = crate::signal::create_backup_guard(final_backup_path.clone());
let (file_count, total_size) = count_files_and_size(source, config)?;
let mut progress = if !quiet {
create_progress_bar(&config.progress, file_count, total_size, force_progress)
} else {
None
};
if let Some(ref mut prog) = progress {
prog.start_scanning();
prog.finish_scanning(file_count, total_size);
}
fs::create_dir_all(&final_backup_path)?;
let mut result = BackupResult::new(source.to_path_buf(), final_backup_path.clone());
let copy_result = copy_directory_contents_with_progress(
source,
&final_backup_path,
config,
&mut result,
&mut progress.as_mut(),
);
if let Err(QbakError::Interrupted) = copy_result {
if let Some(ref mut prog) = progress {
prog.finish();
}
return Err(QbakError::Interrupted);
}
copy_result?;
if let Some(ref mut prog) = progress {
prog.finish();
}
let duration = start_time.elapsed();
result.duration = duration;
_operation_guard.complete();
Ok(result)
}
pub fn count_files_and_size_with_progress(source: &Path, config: &Config) -> Result<(usize, u64)> {
let mut progress = create_progress_bar(&config.progress, 0, 0, true);
if let Some(ref mut prog) = progress {
prog.start_scanning();
}
let result = count_files_and_size_recursive(source, config, &mut progress.as_mut());
if let Some(ref mut prog) = progress {
prog.finish();
}
result
}
pub fn count_files_and_size(source: &Path, config: &Config) -> Result<(usize, u64)> {
let mut none_progress = None;
count_files_and_size_recursive(source, config, &mut none_progress)
}
fn count_files_and_size_recursive(
dir: &Path,
config: &Config,
progress: &mut Option<&mut BackupProgress>,
) -> Result<(usize, u64)> {
let mut total_files = 0;
let mut total_size = 0;
for entry in fs::read_dir(dir)? {
if crate::signal::is_interrupted() {
return Err(QbakError::Interrupted);
}
let entry = entry?;
let path = entry.path();
if !config.include_hidden && is_hidden(&path) {
continue;
}
let metadata = entry.metadata()?;
if metadata.is_file() {
total_files += 1;
total_size += metadata.len();
if let Some(ref mut p) = progress {
if total_files % 100 == 0 {
p.update_scan_progress(total_files, &path);
}
}
} else if metadata.is_dir() {
let (sub_files, sub_size) = count_files_and_size_recursive(&path, config, progress)?;
total_files += sub_files;
total_size += sub_size;
}
}
Ok((total_files, total_size))
}
fn copy_directory_contents_with_progress(
source_dir: &Path,
backup_dir: &Path,
config: &Config,
result: &mut BackupResult,
progress: &mut Option<&mut BackupProgress>,
) -> Result<()> {
for entry in fs::read_dir(source_dir)? {
if crate::signal::is_interrupted() {
return Err(QbakError::Interrupted);
}
let entry = entry?;
let source_path = entry.path();
let filename = source_path.file_name().unwrap();
let backup_path = backup_dir.join(filename);
if !config.include_hidden && is_hidden(&source_path) {
continue;
}
let metadata = entry.metadata()?;
if metadata.is_file() {
copy_file_to_backup(&source_path, &backup_path, config, result)?;
if let Some(ref mut prog) = progress {
prog.update_backup_progress(
result.files_processed,
result.total_size,
&source_path,
);
}
} else if metadata.is_dir() {
fs::create_dir_all(&backup_path)?;
copy_directory_contents_with_progress(
&source_path,
&backup_path,
config,
result,
progress,
)?;
} else if metadata.file_type().is_symlink() {
handle_symlink_with_progress(&source_path, &backup_path, config, result, progress)?;
}
}
Ok(())
}
fn handle_symlink_with_progress(
source: &Path,
backup: &Path,
config: &Config,
result: &mut BackupResult,
progress: &mut Option<&mut BackupProgress>,
) -> Result<()> {
if config.follow_symlinks {
let target = fs::read_link(source)?;
let resolved_target = if target.is_absolute() {
target
} else {
source.parent().unwrap_or(Path::new(".")).join(target)
};
if resolved_target.exists() {
let metadata = fs::metadata(&resolved_target)?;
if metadata.is_file() {
copy_file_to_backup(&resolved_target, backup, config, result)?;
if let Some(ref mut prog) = progress {
prog.update_backup_progress(result.files_processed, result.total_size, source);
}
} else if metadata.is_dir() {
fs::create_dir_all(backup)?;
copy_directory_contents_with_progress(
&resolved_target,
backup,
config,
result,
progress,
)?;
}
}
} else {
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let target = fs::read_link(source)?;
symlink(target, backup)?;
}
#[cfg(not(unix))]
{
let target = fs::read_link(source)?;
let resolved_target = if target.is_absolute() {
target
} else {
source.parent().unwrap_or(Path::new(".")).join(target)
};
if resolved_target.exists() && resolved_target.is_file() {
copy_file_to_backup(&resolved_target, backup, config, result)?;
if let Some(ref mut prog) = progress {
prog.update_backup_progress(result.files_processed, result.total_size, source);
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::default_config;
use std::fs::File;
use std::io::Write;
use tempfile::tempdir;
#[test]
fn test_backup_file() {
let dir = tempdir().unwrap();
let source_path = dir.path().join("test.txt");
let mut file = File::create(&source_path).unwrap();
writeln!(file, "Hello, World!").unwrap();
let config = default_config();
let result = backup_file(&source_path, &config).unwrap();
assert!(result.backup_path.exists());
assert_eq!(result.files_processed, 1);
assert_eq!(result.total_size, 14);
let backup_content = fs::read_to_string(&result.backup_path).unwrap();
assert_eq!(backup_content, "Hello, World!\n");
let summary = result.summary();
assert!(summary.contains("Created backup:"));
assert!(summary.contains("14 B"));
}
#[test]
fn test_backup_file_nonexistent() {
let dir = tempdir().unwrap();
let source_path = dir.path().join("nonexistent.txt");
let config = default_config();
let result = backup_file(&source_path, &config);
assert!(result.is_err());
match result.unwrap_err() {
QbakError::SourceNotFound { path } => assert_eq!(path, source_path),
_ => panic!("Expected SourceNotFound error"),
}
}
#[test]
fn test_backup_directory() {
let dir = tempdir().unwrap();
let source_dir = dir.path().join("source");
fs::create_dir_all(&source_dir).unwrap();
File::create(source_dir.join("file1.txt")).unwrap();
File::create(source_dir.join("file2.txt")).unwrap();
let subdir = source_dir.join("subdir");
fs::create_dir_all(&subdir).unwrap();
File::create(subdir.join("file3.txt")).unwrap();
let config = default_config();
let result = backup_directory(&source_dir, &config, false).unwrap();
assert!(result.backup_path.exists());
assert!(result.backup_path.is_dir());
assert_eq!(result.files_processed, 3);
assert!(result.backup_path.join("file1.txt").exists());
assert!(result.backup_path.join("file2.txt").exists());
assert!(result.backup_path.join("subdir").join("file3.txt").exists());
let summary = result.summary();
assert!(summary.contains("Created backup:"));
assert!(summary.contains("3 files"));
}
#[test]
fn test_backup_directory_on_file() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("file.txt");
File::create(&file_path).unwrap();
let config = default_config();
let result = backup_directory(&file_path, &config, false);
assert!(result.is_err());
match result.unwrap_err() {
QbakError::Validation { message } => assert!(message.contains("not a directory")),
_ => panic!("Expected Validation error"),
}
}
#[test]
fn test_backup_collision_resolution() {
let dir = tempdir().unwrap();
let source_path = dir.path().join("test.txt");
File::create(&source_path).unwrap();
let config = default_config();
let result1 = backup_file(&source_path, &config).unwrap();
assert!(result1.backup_path.exists());
let backup_name = result1.backup_path.file_name().unwrap().to_str().unwrap();
let collision_path = dir.path().join(backup_name);
File::create(&collision_path).unwrap();
}
#[test]
fn test_hidden_file_handling() {
let dir = tempdir().unwrap();
let source_dir = dir.path().join("source");
fs::create_dir_all(&source_dir).unwrap();
File::create(source_dir.join("visible.txt")).unwrap();
File::create(source_dir.join(".hidden.txt")).unwrap();
let mut config = default_config();
config.include_hidden = true;
let result = backup_directory(&source_dir, &config, false).unwrap();
assert_eq!(result.files_processed, 2);
assert!(result.backup_path.join(".hidden.txt").exists());
fs::remove_dir_all(&result.backup_path).unwrap();
config.include_hidden = false;
let result = backup_directory(&source_dir, &config, false).unwrap();
assert_eq!(result.files_processed, 1);
assert!(!result.backup_path.join(".hidden.txt").exists());
assert!(result.backup_path.join("visible.txt").exists());
}
#[test]
fn test_backup_empty_directory() {
let dir = tempdir().unwrap();
let source_dir = dir.path().join("empty_source");
fs::create_dir_all(&source_dir).unwrap();
let config = default_config();
let result = backup_directory(&source_dir, &config, false).unwrap();
assert!(result.backup_path.exists());
assert!(result.backup_path.is_dir());
assert_eq!(result.files_processed, 0);
assert_eq!(result.total_size, 0);
}
#[test]
fn test_backup_file_with_permissions_disabled() {
let dir = tempdir().unwrap();
let source_path = dir.path().join("test.txt");
let mut file = File::create(&source_path).unwrap();
writeln!(file, "Content").unwrap();
let mut config = default_config();
config.preserve_permissions = false;
let result = backup_file(&source_path, &config).unwrap();
assert!(result.backup_path.exists());
let backup_content = fs::read_to_string(&result.backup_path).unwrap();
assert_eq!(backup_content, "Content\n");
}
#[test]
fn test_backup_deeply_nested_directory() {
let dir = tempdir().unwrap();
let source_dir = dir.path().join("source");
let deep_path = source_dir.join("a").join("b").join("c").join("d").join("e");
fs::create_dir_all(&deep_path).unwrap();
fs::write(source_dir.join("root.txt"), "root").unwrap();
fs::write(source_dir.join("a").join("a.txt"), "a").unwrap();
fs::write(source_dir.join("a").join("b").join("b.txt"), "b").unwrap();
fs::write(deep_path.join("deep.txt"), "deep").unwrap();
let config = default_config();
let result = backup_directory(&source_dir, &config, false).unwrap();
assert_eq!(result.files_processed, 4);
assert!(result.backup_path.join("root.txt").exists());
assert!(result.backup_path.join("a").join("a.txt").exists());
assert!(result
.backup_path
.join("a")
.join("b")
.join("b.txt")
.exists());
assert!(result
.backup_path
.join("a")
.join("b")
.join("c")
.join("d")
.join("e")
.join("deep.txt")
.exists());
}
#[test]
fn test_backup_file_large_content() {
let dir = tempdir().unwrap();
let source_path = dir.path().join("large.txt");
let large_content = "x".repeat(10000);
fs::write(&source_path, &large_content).unwrap();
let config = default_config();
let result = backup_file(&source_path, &config).unwrap();
assert!(result.backup_path.exists());
assert_eq!(result.total_size, 10000);
let backup_content = fs::read_to_string(&result.backup_path).unwrap();
assert_eq!(backup_content, large_content);
}
#[test]
fn test_backup_result_new() {
let source = PathBuf::from("/source/path");
let backup = PathBuf::from("/backup/path");
let result = BackupResult::new(source.clone(), backup.clone());
assert_eq!(result.source_path, source);
assert_eq!(result.backup_path, backup);
assert_eq!(result.files_processed, 0);
assert_eq!(result.total_size, 0);
assert_eq!(result.duration.as_secs(), 0);
}
#[test]
fn test_backup_result_summary_single_file() {
let mut result =
BackupResult::new(PathBuf::from("source.txt"), PathBuf::from("backup.txt"));
result.files_processed = 1;
result.total_size = 1024;
let summary = result.summary();
assert!(summary.contains("Created backup:"));
assert!(summary.contains("1.0 KB"));
assert!(!summary.contains("files")); }
#[test]
fn test_backup_result_summary_multiple_files() {
let mut result =
BackupResult::new(PathBuf::from("source_dir"), PathBuf::from("backup_dir"));
result.files_processed = 5;
result.total_size = 2048;
let summary = result.summary();
assert!(summary.contains("Created backup:"));
assert!(summary.contains("5 files"));
assert!(summary.contains("2.0 KB"));
}
#[test]
fn test_backup_directory_with_symlinks() {
let dir = tempdir().unwrap();
let source_dir = dir.path().join("source");
fs::create_dir_all(&source_dir).unwrap();
fs::write(source_dir.join("regular.txt"), "regular content").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let target = source_dir.join("regular.txt");
let link = source_dir.join("link.txt");
symlink(&target, &link).unwrap();
let mut config = default_config();
config.follow_symlinks = true;
let result = backup_directory(&source_dir, &config, false).unwrap();
assert_eq!(result.files_processed, 2);
assert!(result.backup_path.join("regular.txt").exists());
assert!(result.backup_path.join("link.txt").exists());
fs::remove_dir_all(&result.backup_path).unwrap();
config.follow_symlinks = false;
let result = backup_directory(&source_dir, &config, false).unwrap();
assert!(result.backup_path.join("regular.txt").exists());
}
}
#[test]
fn test_cleanup_temp_files() {
let dir = tempdir().unwrap();
let temp1 = dir.path().join(".qbak_temp_12345_file1.txt");
let temp2 = dir.path().join(".qbak_temp_67890_file2.txt");
let normal = dir.path().join("normal_file.txt");
File::create(&temp1).unwrap();
File::create(&temp2).unwrap();
File::create(&normal).unwrap();
assert!(cleanup_temp_files(dir.path()).is_ok());
assert!(!temp1.exists());
assert!(!temp2.exists());
assert!(normal.exists());
}
#[test]
fn test_cleanup_temp_files_nonexistent_dir() {
let nonexistent = Path::new("/nonexistent/directory");
let result = cleanup_temp_files(nonexistent);
assert!(result.is_ok()); }
#[test]
fn test_backup_file_zero_size() {
let dir = tempdir().unwrap();
let source_path = dir.path().join("empty.txt");
File::create(&source_path).unwrap();
let config = default_config();
let result = backup_file(&source_path, &config).unwrap();
assert!(result.backup_path.exists());
assert_eq!(result.total_size, 0);
assert_eq!(result.files_processed, 1);
}
#[test]
fn test_count_files_recursive() {
let dir = tempdir().unwrap();
let source_dir = dir.path().join("source");
fs::create_dir_all(&source_dir).unwrap();
File::create(source_dir.join("file1.txt")).unwrap();
File::create(source_dir.join("file2.txt")).unwrap();
let subdir = source_dir.join("subdir");
fs::create_dir_all(&subdir).unwrap();
File::create(subdir.join("file3.txt")).unwrap();
File::create(subdir.join("file4.txt")).unwrap();
let config = default_config();
let count = count_files_recursive(&source_dir, &config).unwrap();
assert_eq!(count, 4);
}
#[test]
fn test_count_files_recursive_with_hidden() {
let dir = tempdir().unwrap();
let source_dir = dir.path().join("source");
fs::create_dir_all(&source_dir).unwrap();
File::create(source_dir.join("file1.txt")).unwrap();
File::create(source_dir.join(".hidden")).unwrap();
let mut config = default_config();
config.include_hidden = true;
let count = count_files_recursive(&source_dir, &config).unwrap();
assert_eq!(count, 2);
config.include_hidden = false;
let count = count_files_recursive(&source_dir, &config).unwrap();
assert_eq!(count, 1);
}
#[test]
fn test_count_files_recursive_single_file() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("single.txt");
File::create(&file_path).unwrap();
let config = default_config();
let count = count_files_recursive(&file_path, &config).unwrap();
assert_eq!(count, 1);
}
#[test]
fn test_progress_with_verbose_flag() {
crate::signal::reset_for_testing();
let temp_dir = tempfile::tempdir().unwrap();
let source = temp_dir.path().join("source");
std::fs::create_dir(&source).unwrap();
std::fs::write(source.join("file1.txt"), "content1").unwrap();
std::fs::write(source.join("file2.txt"), "content2").unwrap();
std::fs::write(source.join("file3.txt"), "content3").unwrap();
let config = Config::default();
let result = backup_directory(&source, &config, true);
assert!(result.is_ok());
let result2 = backup_directory(&source, &config, false);
assert!(result2.is_ok());
}
#[test]
fn test_backup_directory_mixed_content() {
let dir = tempdir().unwrap();
let source_dir = dir.path().join("mixed");
fs::create_dir_all(&source_dir).unwrap();
File::create(source_dir.join("empty.txt")).unwrap();
fs::write(source_dir.join("small.txt"), "a").unwrap();
fs::write(source_dir.join("medium.txt"), "hello world").unwrap();
let subdir = source_dir.join("sub");
fs::create_dir_all(&subdir).unwrap();
fs::write(subdir.join("sub_file.txt"), "sub content").unwrap();
let config = default_config();
let result = backup_directory(&source_dir, &config, false).unwrap();
assert_eq!(result.files_processed, 4);
assert_eq!(result.total_size, 23);
assert!(result.backup_path.join("empty.txt").exists());
assert!(result.backup_path.join("small.txt").exists());
assert!(result.backup_path.join("medium.txt").exists());
assert!(result.backup_path.join("sub").join("sub_file.txt").exists());
}
#[test]
fn test_backup_operation_guard_cleanup_on_panic() {
let dir = tempdir().unwrap();
let source_path = dir.path().join("test.txt");
fs::write(&source_path, "test content").unwrap();
let config = default_config();
let backup_path = generate_backup_name(&source_path, &config).unwrap();
let final_backup_path = resolve_collision(&backup_path).unwrap();
let result = std::panic::catch_unwind(|| {
let _guard = crate::signal::create_backup_guard(final_backup_path.clone());
fs::create_dir_all(&final_backup_path).unwrap();
fs::write(final_backup_path.join("partial.txt"), "partial").unwrap();
panic!("Simulated interruption");
});
assert!(result.is_err());
assert!(final_backup_path.exists());
}
#[test]
fn test_backup_operation_guard_no_cleanup_on_success() {
let dir = tempdir().unwrap();
let source_path = dir.path().join("test.txt");
fs::write(&source_path, "test content").unwrap();
let config = default_config();
let backup_path = generate_backup_name(&source_path, &config).unwrap();
let final_backup_path = resolve_collision(&backup_path).unwrap();
{
let guard = crate::signal::create_backup_guard(final_backup_path.clone());
fs::create_dir_all(&final_backup_path).unwrap();
fs::write(final_backup_path.join("complete.txt"), "complete").unwrap();
guard.complete();
}
assert!(final_backup_path.exists());
assert!(final_backup_path.join("complete.txt").exists());
}
#[test]
fn test_signal_cleanup_removes_incomplete_backups() {
let dir = tempdir().unwrap();
let backup1 = dir.path().join("backup1-20250630T123456-qbak");
let backup2 = dir.path().join("backup2-20250630T123457-qbak.txt");
fs::create_dir_all(&backup1).unwrap();
fs::write(backup1.join("partial1.txt"), "content").unwrap();
fs::write(&backup2, "partial content").unwrap();
let context = crate::signal::BackupContext::new();
let _guard1 = context.register_operation(backup1.clone());
let _guard2 = context.register_operation(backup2.clone());
let active_ops = context.get_active_operations();
assert!(active_ops.contains(&backup1));
assert!(active_ops.contains(&backup2));
assert!(backup1.exists());
assert!(backup2.exists());
context.cleanup_active_operations_with_mode(true);
assert!(!backup1.exists());
assert!(!backup2.exists());
let remaining_ops = context.get_active_operations();
assert!(remaining_ops.is_empty());
}
#[test]
fn test_backup_file_interrupted_simulation() {
crate::signal::reset_for_testing();
let dir = tempdir().unwrap();
let source_path = dir.path().join("test.txt");
fs::write(&source_path, "test content").unwrap();
let config = default_config();
let backup_path = generate_backup_name(&source_path, &config).unwrap();
let final_backup_path = resolve_collision(&backup_path).unwrap();
let context = crate::signal::BackupContext::new();
let guard = context.register_operation(final_backup_path.clone());
fs::copy(&source_path, &final_backup_path).unwrap();
assert!(final_backup_path.exists());
let active_ops = context.get_active_operations();
assert!(active_ops.contains(&final_backup_path));
context.cleanup_active_operations_with_mode(true);
assert!(!final_backup_path.exists());
drop(guard);
}
#[test]
fn test_backup_directory_interrupted_simulation() {
crate::signal::reset_for_testing();
let dir = tempdir().unwrap();
let source_dir = dir.path().join("source");
fs::create_dir_all(&source_dir).unwrap();
for i in 1..=5 {
fs::write(
source_dir.join(format!("file{i}.txt")),
format!("content{i}"),
)
.unwrap();
}
let config = default_config();
let backup_path = generate_backup_name(&source_dir, &config).unwrap();
let final_backup_path = resolve_collision(&backup_path).unwrap();
let context = crate::signal::BackupContext::new();
let guard = context.register_operation(final_backup_path.clone());
fs::create_dir_all(&final_backup_path).unwrap();
fs::write(final_backup_path.join("file1.txt"), "content1").unwrap();
fs::write(final_backup_path.join("file2.txt"), "content2").unwrap();
assert!(final_backup_path.exists());
assert!(final_backup_path.join("file1.txt").exists());
assert!(final_backup_path.join("file2.txt").exists());
assert!(!final_backup_path.join("file3.txt").exists());
let active_ops = context.get_active_operations();
assert!(active_ops.contains(&final_backup_path));
context.cleanup_active_operations_with_mode(true);
assert!(!final_backup_path.exists());
assert!(!final_backup_path.join("file1.txt").exists());
assert!(!final_backup_path.join("file2.txt").exists());
drop(guard);
}
#[test]
fn test_interrupt_handling_during_backup() {
crate::signal::reset_for_testing();
let dir = tempdir().unwrap();
let source_dir = dir.path().join("source");
fs::create_dir_all(&source_dir).unwrap();
for i in 1..=50 {
fs::write(
source_dir.join(format!("file_{i:03}.txt")),
format!("content for file {i}"),
)
.unwrap();
}
let config = default_config();
let backup_path = generate_backup_name(&source_dir, &config).unwrap();
let final_backup_path = resolve_collision(&backup_path).unwrap();
let context = crate::signal::BackupContext::new();
let guard = context.register_operation(final_backup_path.clone());
fs::create_dir_all(&final_backup_path).unwrap();
fs::copy(
source_dir.join("file_001.txt"),
final_backup_path.join("file_001.txt"),
)
.unwrap();
fs::copy(
source_dir.join("file_002.txt"),
final_backup_path.join("file_002.txt"),
)
.unwrap();
assert!(final_backup_path.exists());
assert!(final_backup_path.join("file_001.txt").exists());
assert!(final_backup_path.join("file_002.txt").exists());
assert!(!final_backup_path.join("file_003.txt").exists());
context.set_interrupted(true);
assert!(context.is_interrupted());
crate::signal::set_interrupt_flag(context.interrupt_flag());
let result = copy_directory_contents(
&source_dir,
&final_backup_path,
&config,
&mut BackupResult::new(source_dir.clone(), final_backup_path.clone()),
false,
);
assert!(result.is_err());
match result.unwrap_err() {
QbakError::Interrupted => {}
other => panic!("Expected Interrupted error, got: {other:?}"),
}
context.cleanup_active_operations_with_mode(true);
assert!(!final_backup_path.exists());
assert!(!final_backup_path.join("file_001.txt").exists());
assert!(!final_backup_path.join("file_002.txt").exists());
drop(guard);
}
#[test]
fn test_large_file_interrupt_and_cleanup() {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
crate::signal::reset_for_testing();
let dir = tempdir().unwrap();
let source_dir = dir.path().join("large_source");
fs::create_dir_all(&source_dir).unwrap();
for i in 1..=2000 {
let content = vec![(i % 256) as u8; 512 * 1024]; fs::write(source_dir.join(format!("file_{i:04}.bin")), content).unwrap();
}
let config = default_config();
let backup_path = generate_backup_name(&source_dir, &config).unwrap();
let final_backup_path = resolve_collision(&backup_path).unwrap();
let context = crate::signal::BackupContext::new();
let backup_started = Arc::new(AtomicBool::new(false));
let backup_result = Arc::new(Mutex::new(None));
crate::signal::set_interrupt_flag(context.interrupt_flag());
let source_dir_clone = source_dir.clone();
let config_clone = config.clone();
let backup_started_clone = backup_started.clone();
let backup_result_clone = backup_result.clone();
let backup_thread = thread::spawn(move || {
backup_started_clone.store(true, Ordering::SeqCst);
let result = backup_directory(&source_dir_clone, &config_clone, false);
if let Ok(mut guard) = backup_result_clone.lock() {
*guard = Some(result);
}
});
while !backup_started.load(Ordering::SeqCst) {
thread::sleep(Duration::from_millis(10));
}
thread::sleep(Duration::from_millis(200));
context.set_interrupted(true);
backup_thread.join().unwrap();
let result_guard = backup_result.lock().unwrap();
let backup_was_interrupted =
matches!(result_guard.as_ref(), Some(Err(QbakError::Interrupted)));
match result_guard.as_ref() {
Some(Ok(_)) => {
assert!(final_backup_path.exists(), "Successful backup should exist");
}
Some(Err(QbakError::Interrupted)) => {
if final_backup_path.exists() {
let active_ops = crate::signal::get_active_operations();
if !active_ops.is_empty() {
crate::signal::cleanup_active_operations_with_mode(true);
assert!(
!final_backup_path.exists(),
"Partial backup should be cleaned up after interrupt"
);
}
}
}
Some(Err(other_error)) => {
panic!("Unexpected error during backup: {other_error:?}");
}
None => {
panic!("Backup thread did not store a result");
}
}
drop(result_guard);
if backup_was_interrupted {
let parent_dir = final_backup_path.parent().unwrap();
if let Ok(entries) = fs::read_dir(parent_dir) {
for entry in entries.flatten() {
let path = entry.path();
let filename = path.file_name().unwrap().to_string_lossy();
if filename.contains(".qbak_temp_") {
panic!("Temp file left behind: {}", path.display());
}
if filename.starts_with(
&source_dir
.file_name()
.unwrap()
.to_string_lossy()
.to_string(),
) && filename.contains("-qbak")
{
panic!(
"Partial backup directory left behind after interrupt: {}",
path.display()
);
}
}
}
}
context.set_interrupted(false);
}
#[test]
fn test_interrupt_during_file_copy_with_chunks() {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
crate::signal::reset_for_testing();
let dir = tempdir().unwrap();
let source_file = dir.path().join("large_test_file.bin");
let dest_file = dir.path().join("dest_file.bin");
eprintln!("Creating 20MB test file...");
let content = vec![0u8; 20 * 1024 * 1024]; fs::write(&source_file, content).unwrap();
let interrupt_flag = Arc::new(AtomicBool::new(false));
crate::signal::set_interrupt_flag(interrupt_flag.clone());
let source_clone = source_file.clone();
let dest_clone = dest_file.clone();
let interrupt_clone = interrupt_flag.clone();
let copy_thread = std::thread::spawn(move || {
std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(100));
eprintln!("Interrupting file copy...");
interrupt_clone.store(true, Ordering::SeqCst);
});
copy_file_with_interrupt_check(&source_clone, &dest_clone)
});
let result = copy_thread.join().unwrap();
match result {
Ok(()) => {
assert!(dest_file.exists(), "File should exist if copy completed");
eprintln!("Copy completed before interrupt (system too fast)");
}
Err(QbakError::Interrupted) => {
if dest_file.exists() {
let source_size = fs::metadata(&source_file).unwrap().len();
let dest_size = fs::metadata(&dest_file).unwrap().len();
if dest_size < source_size {
eprintln!("Successfully interrupted copy - partial file: {dest_size}/{source_size} bytes");
} else {
eprintln!("Copy completed just before interrupt");
}
}
}
Err(other) => {
panic!("Unexpected error during copy: {other:?}");
}
}
interrupt_flag.store(false, Ordering::SeqCst);
}
}