use crate::path::SyncPath;
use clap::{Parser, ValueEnum};
use crate::integrity::ChecksumType;
use crate::compress::CompressionDetection;
use crate::sync::scanner::ScanOptions;
fn parse_sync_path(s: &str) -> Result<SyncPath, String> {
Ok(SyncPath::parse(s))
}
pub fn parse_size(s: &str) -> Result<u64, String> {
let s = s.trim().to_uppercase();
let (num_str, unit) = if let Some(pos) = s.find(|c: char| c.is_alphabetic()) {
(&s[..pos], &s[pos..])
} else {
return s.parse::<u64>().map_err(|e| format!("Invalid size: {}", e));
};
let num: f64 = num_str.trim().parse().map_err(|e| format!("Invalid number '{}': {}", num_str, e))?;
let multiplier: u64 = match unit.trim() {
"B" => 1,
"KB" | "K" => 1024,
"MB" | "M" => 1024 * 1024,
"GB" | "G" => 1024 * 1024 * 1024,
"TB" | "T" => 1024 * 1024 * 1024 * 1024,
_ => return Err(format!("Unknown unit '{}'. Use B, KB, MB, GB, or TB", unit)),
};
let result = num * multiplier as f64;
if result < 0.0 || result > u64::MAX as f64 {
return Err(format!("Size '{}' exceeds maximum (~16 exabytes)", s));
}
Ok(result as u64)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum VerificationMode {
#[default]
None,
Verify,
}
impl VerificationMode {
pub fn checksum_type(&self) -> ChecksumType {
match self {
Self::None => ChecksumType::None,
Self::Verify => ChecksumType::Fast,
}
}
pub fn verify_blocks(&self) -> bool {
false
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
pub enum SymlinkMode {
#[default]
Preserve,
Follow,
Skip,
}
#[derive(Parser, Debug)]
#[command(name = "msy")]
#[command(about = "Modern musl rsync alternative - Fast, parallel file synchronization", long_about = None)]
#[command(version)]
#[command(help_template(
"\
{name} {version} - {about}
{usage-heading} {usage}
{all-args}
"
))]
#[command(after_help = "EXAMPLES:
# Basic sync
sy /source /destination
# Preview changes without applying
sy /source /destination --dry-run
# Mirror mode (delete extra files in destination)
sy /source /destination --delete
# Parallel transfers (20 workers)
sy /source /destination -j 20
# Sync single file
sy /path/to/file.txt /dest/file.txt
# Remote sync (SSH)
sy /local user@host:/remote
sy user@host:/remote /local
# S3 sync
sy /local s3://bucket/path
sy s3://bucket/path /local
# Quiet mode (only errors)
sy /source /destination --quiet
# Bandwidth limiting
sy /source /destination --bwlimit 1MB # Limit to 1 MB/s
sy /source user@host:/dest --bwlimit 500KB # Limit to 500 KB/s
# Verify file integrity after write
sy /source /destination --verify # xxHash3 verification
# Network retry options
sy /source user@host:/dest --retry 5 # Retry up to 5 times on network errors
sy /source user@host:/dest --retry-delay 2 # Start with 2s delay (2s, 4s, 8s, ...)
# Resume interrupted transfers
sy /source user@host:/dest --resume # Auto-resume interrupted large files
sy /source user@host:/dest --resume-only # Only resume, don't start new transfers
sy /source user@host:/dest --clear-resume-state # Clear all resume state
For more information: https://github.com/pepa65/msy")]
pub struct Cli {
#[arg(value_parser = parse_sync_path)]
pub source: Option<SyncPath>,
#[arg(value_parser = parse_sync_path)]
pub destination: Option<SyncPath>,
#[arg(short = 'n', long)]
pub dry_run: bool,
#[arg(long)]
pub diff: bool,
#[arg(short, long)]
pub delete: bool,
#[arg(long, default_value = "50")]
pub delete_threshold: u8,
#[arg(long)]
pub trash: bool,
#[arg(long)]
pub force_delete: bool,
#[arg(short, long, action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(short, long)]
pub quiet: bool,
#[arg(long)]
pub perf: bool,
#[arg(long)]
pub per_file_progress: bool,
#[arg(short = 'j', long, default_value = "10")]
pub parallel: usize,
#[arg(long, default_value = "100")]
pub max_errors: usize,
#[arg(long, value_parser = parse_size)]
pub min_size: Option<u64>,
#[arg(long, value_parser = parse_size)]
pub max_size: Option<u64>,
#[arg(long)]
pub exclude: Vec<String>,
#[arg(long)]
pub include: Vec<String>,
#[arg(long, allow_hyphen_values = true)]
pub filter: Vec<String>,
#[arg(long)]
pub exclude_from: Option<std::path::PathBuf>,
#[arg(long)]
pub include_from: Option<std::path::PathBuf>,
#[arg(long)]
pub ignore_template: Vec<String>,
#[arg(long, value_parser = parse_size)]
pub bwlimit: Option<u64>,
#[arg(long, overrides_with = "no_resume")]
resume: bool,
#[arg(long, overrides_with = "resume")]
pub no_resume: bool,
#[arg(long)]
pub resume_only: bool,
#[arg(long)]
pub clear_resume_state: bool,
#[arg(long)]
pub stream: bool,
#[arg(long, default_value = "100")]
pub checkpoint_files: usize,
#[arg(long, value_parser = parse_size, default_value = "104857600")]
pub checkpoint_bytes: u64,
#[arg(long)]
pub clean_state: bool,
#[arg(long, default_value = "false", action = clap::ArgAction::Set)]
pub use_cache: bool,
#[arg(long)]
pub clear_cache: bool,
#[arg(long, default_value = "false", action = clap::ArgAction::Set)]
pub checksum_db: bool,
#[arg(long)]
pub clear_checksum_db: bool,
#[arg(long)]
pub prune_checksum_db: bool,
#[arg(long)]
pub verify: bool,
#[arg(short = 'z', long)]
pub compress: bool,
#[arg(long, value_enum, default_value = "auto")]
pub compression_detection: CompressionDetection,
#[arg(long, value_enum, default_value = "preserve")]
pub links: SymlinkMode,
#[arg(short = 'L', long)]
pub copy_links: bool,
#[arg(short = 'X', long)]
pub preserve_xattrs: bool,
#[arg(short = 'H', long)]
pub preserve_hardlinks: bool,
#[arg(short = 'A', long)]
pub preserve_acls: bool,
#[arg(short = 'F', long)]
pub preserve_flags: bool,
#[arg(short = 'p', long)]
pub preserve_permissions: bool,
#[arg(short = 't', long)]
pub preserve_times: bool,
#[arg(short = 'g', long)]
pub preserve_group: bool,
#[arg(short = 'o', long)]
pub preserve_owner: bool,
#[arg(short = 'D', long)]
pub preserve_devices: bool,
#[arg(short = 'a', long)]
pub archive: bool,
#[arg(long)]
pub gitignore: bool,
#[arg(long)]
pub exclude_vcs: bool,
#[arg(long)]
pub ignore_times: bool,
#[arg(long)]
pub size_only: bool,
#[arg(short = 'c', long)]
pub checksum: bool,
#[arg(short = 'u', long)]
pub update: bool,
#[arg(long)]
pub ignore_existing: bool,
#[arg(long)]
pub verify_only: bool,
#[arg(long)]
pub json: bool,
#[arg(short = 'w', long)]
pub watch: bool,
#[arg(long)]
pub no_hooks: bool,
#[arg(long)]
pub abort_on_hook_failure: bool,
#[arg(long)]
pub profile: Option<String>,
#[arg(long)]
pub list_profiles: bool,
#[arg(long)]
pub show_profile: Option<String>,
#[arg(long)]
pub bidirectional: bool,
#[arg(long, default_value = "newer")]
pub conflict_resolve: String,
#[arg(long, default_value = "50")]
pub max_delete: u8,
#[arg(long)]
pub clear_bisync_state: bool,
#[arg(long)]
pub force_resync: bool,
#[arg(long, default_value = "3")]
pub retry: u32,
#[arg(long, default_value = "1")]
pub retry_delay: u64,
#[arg(long, hide = true)]
pub server: bool,
#[arg(short = 'r', hide = true)]
pub recursive: bool,
}
impl Cli {
pub fn validate(&self) -> anyhow::Result<()> {
if let (Some(min), Some(max)) = (self.min_size, self.max_size)
&& min > max
{
anyhow::bail!("--min-size ({}) cannot be greater than --max-size ({})", min, max);
}
let comparison_flags = [self.ignore_times, self.size_only, self.checksum];
let enabled_count = comparison_flags.iter().filter(|&&x| x).count();
if enabled_count > 1 {
anyhow::bail!("--ignore-times, --size-only, and --checksum are mutually exclusive");
}
if self.delete_threshold > 100 {
anyhow::bail!("--delete-threshold must be between 0 and 100 (got: {})", self.delete_threshold);
}
if self.verify_only {
if self.delete {
anyhow::bail!("--verify-only cannot be used with --delete (read-only mode)");
}
if self.watch {
anyhow::bail!("--verify-only cannot be used with --watch (read-only mode)");
}
if self.dry_run {
anyhow::bail!("--verify-only is already read-only, --dry-run is redundant");
}
}
if self.bidirectional {
if self.max_delete > 100 {
anyhow::bail!("--max-delete must be between 0 and 100 (got: {})", self.max_delete);
}
let valid_strategies = ["newer", "larger", "smaller", "source", "dest", "rename"];
if !valid_strategies.contains(&self.conflict_resolve.as_str()) {
anyhow::bail!("Invalid --conflict-resolve strategy '{}'. Valid options: {}", self.conflict_resolve, valid_strategies.join(", "));
}
if self.verify_only {
anyhow::bail!("--bidirectional cannot be used with --verify-only (conflicts with sync logic)");
}
if self.watch {
anyhow::bail!("--bidirectional with --watch is not yet supported (deferred to future version)");
}
let source_is_s3 = self.source.as_ref().is_some_and(|p| p.is_s3());
let dest_is_s3 = self.destination.as_ref().is_some_and(|p| p.is_s3());
if source_is_s3 || dest_is_s3 {
anyhow::bail!("--bidirectional does not support S3 paths (use unidirectional sync instead)");
}
}
if self.list_profiles || self.show_profile.is_some() || self.server {
return Ok(());
}
if self.profile.is_none() && (self.source.is_none() || self.destination.is_none()) {
anyhow::bail!("Source and destination are required (or use --profile)");
}
if let Some(source) = &self.source
&& source.is_local()
{
let path = source.path();
if !path.exists() {
anyhow::bail!("Source path does not exist: {}", source);
}
}
Ok(())
}
pub fn verification_mode(&self) -> VerificationMode {
if self.verify { VerificationMode::Verify } else { VerificationMode::None }
}
pub fn symlink_mode(&self) -> SymlinkMode {
if self.copy_links { SymlinkMode::Follow } else { self.links }
}
pub fn scan_options(&self) -> ScanOptions {
let respect_gitignore = self.gitignore;
let include_git_dir = !self.exclude_vcs;
ScanOptions { respect_gitignore, include_git_dir }
}
pub fn resume(&self) -> bool {
if self.no_resume {
false
} else {
true
}
}
pub fn is_single_file(&self) -> bool {
self.source.as_ref().is_some_and(|s| s.is_local() && s.path().is_file())
}
pub fn log_level(&self) -> tracing::Level {
if self.quiet || self.json {
return tracing::Level::ERROR;
}
match self.verbose {
0 => tracing::Level::INFO,
1 => tracing::Level::DEBUG,
_ => tracing::Level::TRACE,
}
}
#[allow(dead_code)] pub fn should_preserve_permissions(&self) -> bool {
self.archive || self.preserve_permissions
}
#[allow(dead_code)] pub fn should_preserve_times(&self) -> bool {
self.archive || self.preserve_times
}
#[allow(dead_code)] pub fn should_preserve_group(&self) -> bool {
self.archive || self.preserve_group
}
#[allow(dead_code)] pub fn should_preserve_owner(&self) -> bool {
self.archive || self.preserve_owner
}
#[allow(dead_code)] pub fn should_preserve_devices(&self) -> bool {
self.archive || self.preserve_devices
}
#[allow(dead_code)] pub fn should_preserve_symlinks(&self) -> bool {
if self.archive && !self.copy_links { true } else { self.symlink_mode() == SymlinkMode::Preserve }
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
#[test]
fn test_validate_source_exists() {
let temp = TempDir::new().unwrap();
let cli = Cli {
source: Some(SyncPath::Local { path: temp.path().to_path_buf(), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 0,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
min_size: None,
max_size: None,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Preserve,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: false,
preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: false,
gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: false,
size_only: false,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
assert!(cli.validate().is_ok());
}
#[test]
fn test_validate_source_not_exists() {
let cli = Cli {
source: Some(SyncPath::Local { path: PathBuf::from("/nonexistent/path"), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 0,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
min_size: None,
max_size: None,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Preserve,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: false,
preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: false,
gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: false,
size_only: false,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
let result = cli.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("does not exist"));
}
#[test]
fn test_validate_source_is_file() {
let temp = TempDir::new().unwrap();
let file_path = temp.path().join("file.txt");
fs::write(&file_path, "content").unwrap();
let cli = Cli {
source: Some(SyncPath::Local { path: file_path.clone(), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 0,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Preserve,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: false,
preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: false,
gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: false,
size_only: false,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
min_size: None,
max_size: None,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
assert!(cli.validate().is_ok());
assert!(cli.is_single_file());
}
#[test]
fn test_validate_remote_source() {
let cli = Cli {
source: Some(SyncPath::Remote {
host: "server".to_string(),
user: Some("user".to_string()),
path: PathBuf::from("/remote/path"),
has_trailing_slash: false,
}),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 0,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Preserve,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: false,
preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: false,
gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: false,
size_only: false,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
min_size: None,
max_size: None,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
assert!(cli.validate().is_ok());
}
#[test]
fn test_log_level_quiet() {
let cli = Cli {
source: Some(SyncPath::Local { path: PathBuf::from("/tmp/src"), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 0,
quiet: true,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Preserve,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: false,
preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: false,
gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: false,
size_only: false,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
min_size: None,
max_size: None,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
assert_eq!(cli.log_level(), tracing::Level::ERROR);
}
#[test]
fn test_log_level_default() {
let cli = Cli {
source: Some(SyncPath::Local { path: PathBuf::from("/tmp/src"), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 0,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Preserve,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: false,
preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: false,
gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: false,
size_only: false,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
min_size: None,
max_size: None,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
assert_eq!(cli.log_level(), tracing::Level::INFO);
}
#[test]
fn test_log_level_verbose() {
let cli = Cli {
source: Some(SyncPath::Local { path: PathBuf::from("/tmp/src"), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 1,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Preserve,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: false,
preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: false,
gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: false,
size_only: false,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
min_size: None,
max_size: None,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
assert_eq!(cli.log_level(), tracing::Level::DEBUG);
}
#[test]
fn test_log_level_very_verbose() {
let cli = Cli {
source: Some(SyncPath::Local { path: PathBuf::from("/tmp/src"), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 2,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Preserve,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: false,
preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: false,
gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: false,
size_only: false,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
min_size: None,
max_size: None,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
assert_eq!(cli.log_level(), tracing::Level::TRACE);
}
#[test]
fn test_parse_size() {
assert_eq!(parse_size("1024").unwrap(), 1024);
assert_eq!(parse_size("1KB").unwrap(), 1024);
assert_eq!(parse_size("1MB").unwrap(), 1024 * 1024);
assert_eq!(parse_size("1GB").unwrap(), 1024 * 1024 * 1024);
assert_eq!(parse_size("1.5MB").unwrap(), (1.5 * 1024.0 * 1024.0) as u64);
assert_eq!(parse_size("500KB").unwrap(), 500 * 1024);
assert_eq!(parse_size("1mb").unwrap(), 1024 * 1024);
assert_eq!(parse_size("1Mb").unwrap(), 1024 * 1024);
assert_eq!(parse_size("1K").unwrap(), 1024);
assert_eq!(parse_size("1M").unwrap(), 1024 * 1024);
assert_eq!(parse_size("1G").unwrap(), 1024 * 1024 * 1024);
}
#[test]
fn test_size_filter_validation() {
let cli = Cli {
source: Some(SyncPath::Local { path: PathBuf::from("/tmp/src"), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 0,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Preserve,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: false,
preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: false,
gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: false,
size_only: false,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
min_size: Some(1024 * 1024), max_size: Some(500 * 1024), retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
let result = cli.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("min-size"));
}
#[test]
fn test_verification_mode_default() {
let cli = Cli {
source: Some(SyncPath::Local { path: PathBuf::from("/tmp/src"), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 0,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Preserve,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: false,
preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: false,
gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: false,
size_only: false,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
min_size: None,
max_size: None,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
assert_eq!(cli.verification_mode(), VerificationMode::None);
}
#[test]
fn test_verification_mode_verify_flag_enabled() {
let cli = Cli {
source: Some(SyncPath::Local { path: PathBuf::from("/tmp/src"), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 0,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: true, resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Preserve,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: false,
preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: false,
gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: false,
size_only: false,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
min_size: None,
max_size: None,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
assert_eq!(cli.verification_mode(), VerificationMode::Verify);
}
#[test]
fn test_verification_mode_checksum_type_mapping() {
assert_eq!(VerificationMode::None.checksum_type(), ChecksumType::None);
assert_eq!(VerificationMode::Verify.checksum_type(), ChecksumType::Fast);
}
#[test]
fn test_verification_mode_verify_blocks() {
assert!(!VerificationMode::None.verify_blocks());
assert!(!VerificationMode::Verify.verify_blocks());
}
#[test]
fn test_symlink_mode_default() {
let cli = Cli {
source: Some(SyncPath::Local { path: PathBuf::from("/tmp/src"), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 0,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Preserve,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: false,
preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: false,
gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: false,
size_only: false,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
min_size: None,
max_size: None,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
assert_eq!(cli.symlink_mode(), SymlinkMode::Preserve);
}
#[test]
fn test_symlink_mode_copy_links_override() {
let cli = Cli {
source: Some(SyncPath::Local { path: PathBuf::from("/tmp/src"), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 0,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Skip, copy_links: true, preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: false,
preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: false,
gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: false,
size_only: false,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
min_size: None,
max_size: None,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
assert_eq!(cli.symlink_mode(), SymlinkMode::Follow);
}
#[test]
fn test_symlink_mode_skip() {
let cli = Cli {
source: Some(SyncPath::Local { path: PathBuf::from("/tmp/src"), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 0,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Skip,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: false,
preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: false,
gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: false,
size_only: false,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
min_size: None,
max_size: None,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
assert_eq!(cli.symlink_mode(), SymlinkMode::Skip);
}
#[test]
fn test_archive_mode_enables_all_flags() {
let cli = Cli {
source: Some(SyncPath::Local { path: PathBuf::from("/tmp/src"), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 0,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Preserve,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: false,
preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: true, gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: false,
size_only: false,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
min_size: None,
max_size: None,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
assert!(cli.should_preserve_permissions());
assert!(cli.should_preserve_times());
assert!(cli.should_preserve_group());
assert!(cli.should_preserve_owner());
assert!(cli.should_preserve_devices());
assert!(cli.should_preserve_symlinks());
}
#[test]
fn test_individual_preserve_flags() {
let cli = Cli {
source: Some(SyncPath::Local { path: PathBuf::from("/tmp/src"), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 0,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Preserve,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: true, preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: false,
gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: false,
size_only: false,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
min_size: None,
max_size: None,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
assert!(cli.should_preserve_permissions());
assert!(!cli.should_preserve_times());
assert!(!cli.should_preserve_group());
assert!(!cli.should_preserve_owner());
assert!(!cli.should_preserve_devices());
}
#[test]
fn test_explicit_flag_overrides_with_archive() {
let cli = Cli {
source: Some(SyncPath::Local { path: PathBuf::from("/tmp/src"), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 0,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Preserve,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: true, preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: true, gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: false,
size_only: false,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
min_size: None,
max_size: None,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
assert!(cli.should_preserve_permissions());
assert!(cli.should_preserve_times());
assert!(cli.should_preserve_group());
assert!(cli.should_preserve_owner());
assert!(cli.should_preserve_devices());
}
#[test]
fn test_comparison_flags_mutually_exclusive() {
let cli = Cli {
source: Some(SyncPath::Local { path: PathBuf::from("/tmp/src"), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 0,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Preserve,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: false,
preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: false,
gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: true, size_only: true,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
min_size: None,
max_size: None,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
let result = cli.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("mutually exclusive"));
}
#[test]
fn test_ignore_times_flag_alone() {
let temp = TempDir::new().unwrap();
let cli = Cli {
source: Some(SyncPath::Local { path: temp.path().to_path_buf(), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 0,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Preserve,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: false,
preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: false,
gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: true, size_only: false,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
min_size: None,
max_size: None,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
assert!(cli.validate().is_ok());
assert!(cli.ignore_times);
}
#[test]
fn test_checksum_flag_alone() {
let temp = TempDir::new().unwrap();
let cli = Cli {
source: Some(SyncPath::Local { path: temp.path().to_path_buf(), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 0,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Preserve,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: false,
preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: false,
gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: false,
size_only: false,
checksum: true, verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
min_size: None,
max_size: None,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
};
assert!(cli.validate().is_ok());
assert!(cli.checksum);
}
#[test]
fn test_scan_options_default() {
let cli = create_test_cli();
let options = cli.scan_options();
assert!(!options.respect_gitignore); assert!(options.include_git_dir); }
#[test]
fn test_scan_options_archive_mode() {
let mut cli = create_test_cli();
cli.archive = true;
let options = cli.scan_options();
assert!(!options.respect_gitignore);
assert!(options.include_git_dir);
}
#[test]
fn test_scan_options_explicit_flags() {
let mut cli = create_test_cli();
cli.gitignore = true;
let options = cli.scan_options();
assert!(options.respect_gitignore);
assert!(options.include_git_dir);
let mut cli = create_test_cli();
cli.exclude_vcs = true;
let options = cli.scan_options();
assert!(!options.respect_gitignore); assert!(!options.include_git_dir);
let mut cli = create_test_cli();
cli.gitignore = true;
cli.exclude_vcs = true;
let options = cli.scan_options();
assert!(options.respect_gitignore);
assert!(!options.include_git_dir);
}
fn create_test_cli() -> Cli {
Cli {
source: Some(SyncPath::Local { path: PathBuf::from("/tmp/src"), has_trailing_slash: false }),
destination: Some(SyncPath::Local { path: PathBuf::from("/tmp/dest"), has_trailing_slash: false }),
dry_run: false,
diff: false,
delete: false,
delete_threshold: 50,
trash: false,
force_delete: false,
verbose: 0,
quiet: false,
perf: false,
per_file_progress: false,
parallel: 10,
max_errors: 100,
exclude: vec![],
include: vec![],
filter: vec![],
exclude_from: None,
include_from: None,
ignore_template: vec![],
bwlimit: None,
compress: false,
compression_detection: CompressionDetection::Auto,
verify: false,
resume: false,
no_resume: false,
checkpoint_files: 100,
checkpoint_bytes: 104857600,
clean_state: false,
links: SymlinkMode::Preserve,
copy_links: false,
preserve_xattrs: false,
preserve_hardlinks: false,
preserve_acls: false,
preserve_flags: false,
preserve_permissions: false,
preserve_times: false,
preserve_group: false,
preserve_owner: false,
preserve_devices: false,
archive: false,
gitignore: false,
exclude_vcs: false,
update: false,
ignore_existing: false,
ignore_times: false,
size_only: false,
checksum: false,
verify_only: false,
json: false,
stream: false,
watch: false,
no_hooks: false,
abort_on_hook_failure: false,
profile: None,
list_profiles: false,
show_profile: None,
bidirectional: false,
conflict_resolve: "newer".to_string(),
max_delete: 50,
clear_bisync_state: false,
force_resync: false,
use_cache: false,
clear_cache: false,
checksum_db: false,
clear_checksum_db: false,
prune_checksum_db: false,
min_size: None,
max_size: None,
retry: 3,
retry_delay: 1,
resume_only: false,
clear_resume_state: false,
recursive: false,
server: false,
}
}
}