use crate::algorithm::{DeltaAlgorithm, Match};
use crate::file_list::{
compare_file_lists_with_roots, generate_file_list_with_options, FileOperation,
};
use crate::logging::SyncLogger;
use crate::options::SyncOptions;
use crate::progress::SyncProgress;
use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
pub fn synchronize(
source: PathBuf,
destination: PathBuf,
_threads: usize,
_compress: bool,
) -> Result<()> {
let logger = SyncLogger::new(None, false)?;
logger.log("Starting synchronization...");
logger.log(&format!(" Source: {}", source.display()));
logger.log(&format!(" Destination: {}", destination.display()));
if !destination.exists() {
fs::create_dir_all(&destination).with_context(|| {
format!(
"Failed to create destination directory: {}",
destination.display()
)
})?;
}
let source_metadata = fs::symlink_metadata(&source)
.with_context(|| format!("Failed to get source metadata: {}", source.display()))?;
if source_metadata.is_symlink() {
let target = fs::read_link(&source)
.with_context(|| format!("Failed to read symlink target: {}", source.display()))?;
if destination.is_dir() {
let file_name = source
.file_name()
.ok_or_else(|| anyhow::anyhow!("Source symlink has no name"))?;
let dest_file = destination.join(file_name);
create_symlink(&target, &dest_file)?;
} else {
create_symlink(&target, &destination)?;
}
} else if source_metadata.is_file() && destination.is_dir() {
let file_name = source
.file_name()
.ok_or_else(|| anyhow::anyhow!("Source file has no name"))?;
let dest_file = destination.join(file_name);
sync_single_file(&source, &dest_file, &logger)?;
} else if source_metadata.is_file() && (!destination.exists() || destination.is_file()) {
sync_single_file(&source, &destination, &logger)?;
} else if source_metadata.is_dir() {
let default_options = SyncOptions::default();
sync_directories(&source, &destination, &default_options, &logger)?;
} else {
return Err(anyhow::anyhow!("Invalid source/destination combination"));
}
logger.log("Synchronization completed successfully!");
logger.close();
Ok(())
}
pub fn synchronize_with_options(
source: PathBuf,
destination: PathBuf,
_threads: usize,
mut options: SyncOptions,
) -> Result<()> {
let logger = SyncLogger::new(options.log_file.as_deref(), options.show_eta)?;
let dest_parent = if destination.exists() {
destination.clone()
} else {
destination
.parent()
.ok_or_else(|| anyhow::anyhow!("Destination has no parent directory"))?
.to_path_buf()
};
if let Ok(capabilities) = crate::metadata::detect_filesystem_capabilities(&dest_parent) {
let original_flags = crate::metadata::CopyFlags::from_string(&options.copy_flags);
let filtered_flags =
crate::metadata::filter_copy_flags_for_filesystem(&original_flags, &capabilities);
let mut filtered_out = Vec::new();
if original_flags.owner && !filtered_flags.owner {
filtered_out.push("Owner (O)");
}
if original_flags.security && !filtered_flags.security {
filtered_out.push("Security/Permissions (S)");
}
if original_flags.attributes && !filtered_flags.attributes {
filtered_out.push("Extended Attributes (A)");
}
if original_flags.timestamps && !filtered_flags.timestamps {
filtered_out.push("Timestamps (T)");
}
if !filtered_out.is_empty() {
match capabilities.filesystem_type {
crate::metadata::FilesystemType::Network => {
logger.log(
"Warning: Network filesystem detected. The following copy flags may fail and have been disabled:"
);
}
crate::metadata::FilesystemType::Tmpfs => {
logger.log(
"Warning: Temporary filesystem detected. The following copy flags may fail and have been disabled:"
);
}
_ => {
logger.log(
"Warning: Filesystem limitations detected. The following copy flags have been disabled:"
);
}
}
for flag in &filtered_out {
logger.log(&format!(" - {flag}"));
}
logger.log("Consider using -copyflags DAT for cross-filesystem copies.");
}
let mut new_flags = String::new();
if filtered_flags.data {
new_flags.push('D');
}
if filtered_flags.attributes {
new_flags.push('A');
}
if filtered_flags.timestamps {
new_flags.push('T');
}
if filtered_flags.security {
new_flags.push('S');
}
if filtered_flags.owner {
new_flags.push('O');
}
options.copy_flags = new_flags;
}
if options.dry_run {
logger.log("DRY RUN - would synchronize:");
logger.log(&format!(" Source: {}", source.display()));
logger.log(&format!(" Destination: {}", destination.display()));
logger.close();
return Ok(());
}
let source_metadata = fs::metadata(&source)?;
if source_metadata.is_file() && destination.is_dir() {
let file_name = source
.file_name()
.ok_or_else(|| anyhow::anyhow!("Source file has no name"))?;
let dest_file = destination.join(file_name);
sync_single_file(&source, &dest_file, &logger)?;
} else if source_metadata.is_file() && (!destination.exists() || destination.is_file()) {
sync_single_file(&source, &destination, &logger)?;
} else if source_metadata.is_dir() {
sync_directories(&source, &destination, &options, &logger)?;
} else {
return Err(anyhow::anyhow!("Invalid source/destination combination"));
}
logger.log("Synchronization completed successfully!");
logger.close();
Ok(())
}
fn sync_single_file(source: &Path, destination: &Path, logger: &SyncLogger) -> Result<()> {
logger.log(&format!(
"Syncing file: {} -> {}",
source.display(),
destination.display()
));
let source_data = fs::read(source)
.with_context(|| format!("Failed to read source file: {}", source.display()))?;
if !destination.exists() {
if let Some(parent) = destination.parent() {
fs::create_dir_all(parent).with_context(|| {
format!("Failed to create parent directory: {}", parent.display())
})?;
}
fs::write(destination, &source_data).with_context(|| {
format!(
"Failed to write destination file: {}",
destination.display()
)
})?;
logger.log(&format!(" Copied {} bytes (new file)", source_data.len()));
return Ok(());
}
let dest_data = fs::read(destination)
.with_context(|| format!("Failed to read destination file: {}", destination.display()))?;
let algorithm = DeltaAlgorithm::default();
let checksums = algorithm
.generate_checksums(&dest_data)
.context("Failed to generate checksums for destination")?;
let matches = algorithm
.find_matches(&source_data, &checksums)
.context("Failed to find matches")?;
let new_data = apply_delta(&dest_data, &matches)?;
fs::write(destination, &new_data)
.with_context(|| format!("Failed to write updated file: {}", destination.display()))?;
let literal_bytes: usize = matches
.iter()
.filter_map(|m| match m {
Match::Literal { data, .. } => Some(data.len()),
_ => None,
})
.sum();
let block_matches = matches
.iter()
.filter(|m| matches!(m, Match::Block { .. }))
.count();
logger.log(&format!(
" Transferred {literal_bytes} bytes ({literal_bytes} literal, {block_matches} block matches)"
));
Ok(())
}
fn apply_delta(dest_data: &[u8], matches: &[Match]) -> Result<Vec<u8>> {
let mut result = Vec::new();
for match_item in matches {
match match_item {
Match::Literal { data, .. } => {
result.extend_from_slice(data);
}
Match::Block {
target_offset,
length,
..
} => {
let start = *target_offset as usize;
let end = start + length;
if end <= dest_data.len() {
result.extend_from_slice(&dest_data[start..end]);
} else {
return Err(anyhow::anyhow!(
"Block match extends beyond destination data"
));
}
}
}
}
Ok(result)
}
fn sync_directories(
source: &Path,
destination: &Path,
options: &SyncOptions,
logger: &SyncLogger,
) -> Result<()> {
logger.log(&format!(
"Syncing directory: {} -> {}",
source.display(),
destination.display()
));
let source_files = generate_file_list_with_options(source, options)
.context("Failed to generate source file list")?;
let dest_files = if destination.exists() {
generate_file_list_with_options(destination, options)
.context("Failed to generate destination file list")?
} else {
Vec::new()
};
let operations =
compare_file_lists_with_roots(&source_files, &dest_files, source, destination, options);
let total_files = operations.len() as u64;
let total_bytes: u64 = source_files
.iter()
.filter(|f| !f.is_directory)
.map(|f| f.size)
.sum();
let mut progress = SyncProgress::new(total_files, total_bytes);
for operation in operations {
match operation {
FileOperation::CreateDirectory { path } => {
let dest_path = map_source_to_dest(&path, source, destination)?;
fs::create_dir_all(&dest_path).with_context(|| {
format!("Failed to create directory: {}", dest_path.display())
})?;
progress.update_file_complete(0);
}
FileOperation::Create { path } => {
let dest_path = map_source_to_dest(&path, source, destination)?;
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent)?;
}
let file_size = fs::metadata(&path)?.len();
fs::copy(&path, &dest_path).with_context(|| {
format!(
"Failed to copy file: {} -> {}",
path.display(),
dest_path.display()
)
})?;
progress.update_file_complete(file_size);
}
FileOperation::Update {
path,
use_delta: true,
} => {
let dest_path = map_source_to_dest(&path, source, destination)?;
sync_single_file(&path, &dest_path, logger)?;
let file_size = fs::metadata(&path)?.len();
progress.update_file_complete(file_size);
}
FileOperation::Update {
path,
use_delta: false,
} => {
let dest_path = map_source_to_dest(&path, source, destination)?;
let file_size = fs::metadata(&path)?.len();
fs::copy(&path, &dest_path).with_context(|| {
format!(
"Failed to copy file: {} -> {}",
path.display(),
dest_path.display()
)
})?;
progress.update_file_complete(file_size);
}
FileOperation::Delete { path } => {
let metadata = fs::symlink_metadata(&path)
.with_context(|| format!("Failed to get metadata for: {}", path.display()))?;
if metadata.is_symlink() {
fs::remove_file(&path)
.with_context(|| format!("Failed to delete symlink: {}", path.display()))?;
} else if metadata.is_file() {
fs::remove_file(&path)
.with_context(|| format!("Failed to delete file: {}", path.display()))?;
} else if metadata.is_dir() {
fs::remove_dir_all(&path).with_context(|| {
format!("Failed to delete directory: {}", path.display())
})?;
}
progress.update_file_complete(0);
}
FileOperation::CreateSymlink { path, target } => {
let dest_path = map_source_to_dest(&path, source, destination)?;
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent)?;
}
#[cfg(unix)]
std::os::unix::fs::symlink(&target, &dest_path).with_context(|| {
format!(
"Failed to create symlink: {} -> {}",
dest_path.display(),
target.display()
)
})?;
#[cfg(windows)]
{
let _target_path = if target.is_absolute() {
target.clone()
} else {
path.parent().unwrap_or(Path::new(".")).join(&target)
};
crate::windows_symlinks::create_symlink(&dest_path, &target)?;
}
progress.update_file_complete(0);
}
FileOperation::UpdateSymlink { path, target } => {
let dest_path = map_source_to_dest(&path, source, destination)?;
fs::remove_file(&dest_path).with_context(|| {
format!("Failed to remove existing symlink: {}", dest_path.display())
})?;
#[cfg(unix)]
std::os::unix::fs::symlink(&target, &dest_path).with_context(|| {
format!(
"Failed to update symlink: {} -> {}",
dest_path.display(),
target.display()
)
})?;
#[cfg(windows)]
{
let target_path = if target.is_absolute() {
target.clone()
} else {
path.parent().unwrap_or(Path::new(".")).join(&target)
};
if target_path.is_dir() {
std::os::windows::fs::symlink_dir(&target, &dest_path).with_context(
|| {
format!(
"Failed to update directory symlink: {} -> {}",
dest_path.display(),
target.display()
)
},
)?;
} else {
std::os::windows::fs::symlink_file(&target, &dest_path).with_context(
|| {
format!(
"Failed to update file symlink: {} -> {}",
dest_path.display(),
target.display()
)
},
)?;
}
}
progress.update_file_complete(0);
}
}
}
progress.finish();
Ok(())
}
fn map_source_to_dest(source_file: &Path, source_root: &Path, dest_root: &Path) -> Result<PathBuf> {
let relative = source_file.strip_prefix(source_root).with_context(|| {
format!(
"File {} is not under source root {}",
source_file.display(),
source_root.display()
)
})?;
Ok(dest_root.join(relative))
}
fn create_symlink(target: &Path, destination: &Path) -> Result<()> {
println!(
"Creating symlink: {} -> {}",
destination.display(),
target.display()
);
if let Some(parent) = destination.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create parent directory: {}", parent.display()))?;
}
#[cfg(unix)]
std::os::unix::fs::symlink(target, destination).with_context(|| {
format!(
"Failed to create symlink: {} -> {}",
destination.display(),
target.display()
)
})?;
#[cfg(windows)]
{
crate::windows_symlinks::create_symlink(destination, target)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_sync_single_file_new() -> Result<()> {
let temp_dir = TempDir::new()?;
let source = temp_dir.path().join("source.txt");
let dest = temp_dir.path().join("dest.txt");
fs::write(&source, b"Hello, World!")?;
let logger = SyncLogger::new(None, false)?;
sync_single_file(&source, &dest, &logger)?;
let dest_content = fs::read(&dest)?;
assert_eq!(dest_content, b"Hello, World!");
Ok(())
}
#[test]
fn test_apply_delta() -> Result<()> {
let dest_data = b"Hello, World!";
let matches = vec![
Match::Block {
source_offset: 0,
target_offset: 0,
length: 5,
}, Match::Literal {
offset: 5,
data: b" Rust".to_vec(),
is_compressed: false,
}, Match::Block {
source_offset: 10,
target_offset: 5,
length: 8,
}, ];
let result = apply_delta(dest_data, &matches)?;
assert_eq!(result, b"Hello Rust, World!");
Ok(())
}
}