use std::path::PathBuf;
use clap::{ValueEnum, builder::PossibleValue};
use crate::cli::Cli;
use crate::error::{FastSyncError, Result};
use crate::filter::PathFilter;
use crate::i18n::{tr_current, tr_value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompareMode {
Fast,
Strict,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerifyMode {
None,
Changed,
All,
}
impl VerifyMode {
pub fn verify_changed_files(self) -> bool {
match self {
Self::Changed | Self::All => true,
Self::None => false,
}
}
pub fn verify_all_files(self) -> bool {
match self {
Self::All => true,
Self::None | Self::Changed => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PreserveMode {
Auto,
True,
False,
}
impl PreserveMode {
pub fn enabled(self) -> bool {
match self {
Self::Auto | Self::True => true,
Self::False => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HashAlgorithm {
Blake3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Error,
Warn,
Info,
Debug,
Trace,
}
impl LogLevel {
pub fn as_str(self) -> &'static str {
match self {
Self::Error => "error",
Self::Warn => "warn",
Self::Info => "info",
Self::Debug => "debug",
Self::Trace => "trace",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputMode {
Text,
Json,
}
#[derive(Debug, Clone)]
pub struct SyncConfig {
pub source: PathBuf,
pub target: PathBuf,
pub dry_run: bool,
pub delete: bool,
pub follow_symlinks: bool,
pub compare_mode: CompareMode,
pub hash_algorithm: HashAlgorithm,
pub verify_mode: VerifyMode,
pub sync_metadata: bool,
pub preserve_times: PreserveMode,
pub preserve_permissions: PreserveMode,
pub atomic_write: bool,
pub threads: usize,
pub queue_size: usize,
pub max_errors: usize,
pub stop_on_error: bool,
pub output: OutputMode,
pub log_level: LogLevel,
pub filter: PathFilter,
}
impl SyncConfig {
pub fn syncs_file_metadata(&self) -> bool {
self.sync_metadata && (self.preserve_times.enabled() || self.preserve_permissions.enabled())
}
}
impl TryFrom<Cli> for SyncConfig {
type Error = FastSyncError;
fn try_from(cli: Cli) -> Result<Self> {
if !cli.source.is_dir() {
return Err(FastSyncError::InvalidSource(cli.source));
}
let threads = match cli.threads.as_deref() {
#[allow(non_snake_case)]
None | Some("auto") => default_threads(),
Some(raw) => raw.parse::<usize>().map_err(|err| FastSyncError::Io {
context: tr_value("io.parse_threads", raw),
source: std::io::Error::new(std::io::ErrorKind::InvalidInput, err),
})?,
}
.max(1);
let queue_size = cli.queue_size.unwrap_or_else(|| threads * 4).max(1);
let compare_mode = if cli.strict {
CompareMode::Strict
} else {
cli.compare
};
let filter = PathFilter::from_config(cli.filter.as_ref())?;
Ok(Self {
source: cli.source,
target: cli.target,
dry_run: cli.dry_run,
delete: cli.delete,
follow_symlinks: cli.follow_symlinks,
compare_mode,
hash_algorithm: cli.hash,
verify_mode: cli.verify,
sync_metadata: cli.sync_metadata,
preserve_times: cli.preserve_times,
preserve_permissions: cli.preserve_permissions,
atomic_write: cli.atomic_write,
threads,
queue_size,
max_errors: cli.max_errors,
stop_on_error: cli.stop_on_error,
output: cli.output,
log_level: cli.log_level,
filter,
})
}
}
fn default_threads() -> usize {
std::thread::available_parallelism()
.map(|value| value.get())
.unwrap_or(4)
.clamp(1, 8)
}
const COMPARE_MODE_VARIANTS: [CompareMode; 2] = [CompareMode::Fast, CompareMode::Strict];
const VERIFY_MODE_VARIANTS: [VerifyMode; 3] =
[VerifyMode::None, VerifyMode::Changed, VerifyMode::All];
const PRESERVE_MODE_VARIANTS: [PreserveMode; 3] =
[PreserveMode::Auto, PreserveMode::True, PreserveMode::False];
const HASH_ALGORITHM_VARIANTS: [HashAlgorithm; 1] = [HashAlgorithm::Blake3];
const LOG_LEVEL_VARIANTS: [LogLevel; 5] = [
LogLevel::Error,
LogLevel::Warn,
LogLevel::Info,
LogLevel::Debug,
LogLevel::Trace,
];
const OUTPUT_MODE_VARIANTS: [OutputMode; 2] = [OutputMode::Text, OutputMode::Json];
impl ValueEnum for CompareMode {
fn value_variants<'a>() -> &'a [Self] {
&COMPARE_MODE_VARIANTS
}
fn to_possible_value(&self) -> Option<PossibleValue> {
match self {
Self::Fast => Some(PossibleValue::new("fast").help(tr_current("value.compare.fast"))),
Self::Strict => {
Some(PossibleValue::new("strict").help(tr_current("value.compare.strict")))
}
}
}
}
impl ValueEnum for VerifyMode {
fn value_variants<'a>() -> &'a [Self] {
&VERIFY_MODE_VARIANTS
}
fn to_possible_value(&self) -> Option<PossibleValue> {
match self {
Self::None => Some(PossibleValue::new("none").help(tr_current("value.verify.none"))),
Self::Changed => {
Some(PossibleValue::new("changed").help(tr_current("value.verify.changed")))
}
Self::All => Some(PossibleValue::new("all").help(tr_current("value.verify.all"))),
}
}
}
impl ValueEnum for PreserveMode {
fn value_variants<'a>() -> &'a [Self] {
&PRESERVE_MODE_VARIANTS
}
fn to_possible_value(&self) -> Option<PossibleValue> {
match self {
Self::Auto => Some(PossibleValue::new("auto").help(tr_current("value.preserve.auto"))),
Self::True => Some(PossibleValue::new("true").help(tr_current("value.preserve.true"))),
Self::False => {
Some(PossibleValue::new("false").help(tr_current("value.preserve.false")))
}
}
}
}
impl ValueEnum for HashAlgorithm {
fn value_variants<'a>() -> &'a [Self] {
&HASH_ALGORITHM_VARIANTS
}
fn to_possible_value(&self) -> Option<PossibleValue> {
match self {
Self::Blake3 => {
Some(PossibleValue::new("blake3").help(tr_current("value.hash.blake3")))
}
}
}
}
impl ValueEnum for LogLevel {
fn value_variants<'a>() -> &'a [Self] {
&LOG_LEVEL_VARIANTS
}
fn to_possible_value(&self) -> Option<PossibleValue> {
match self {
Self::Error => Some(PossibleValue::new("error")),
Self::Warn => Some(PossibleValue::new("warn")),
Self::Info => Some(PossibleValue::new("info")),
Self::Debug => Some(PossibleValue::new("debug")),
Self::Trace => Some(PossibleValue::new("trace")),
}
}
}
impl ValueEnum for OutputMode {
fn value_variants<'a>() -> &'a [Self] {
&OUTPUT_MODE_VARIANTS
}
fn to_possible_value(&self) -> Option<PossibleValue> {
match self {
Self::Text => Some(PossibleValue::new("text")),
Self::Json => Some(PossibleValue::new("json")),
}
}
}
#[cfg(test)]
mod tests {
use tempfile::tempdir;
use crate::cli::Cli;
use crate::error::FastSyncError;
use super::*;
#[test]
fn cli_config_rejects_missing_source() -> std::result::Result<(), Box<dyn std::error::Error>> {
let root = tempdir()?;
let missing = root.path().join("missing");
let target = root.path().join("target");
let cli = Cli::parse_from([
"fastsync",
missing.to_str().expect("temp path should be UTF-8"),
target.to_str().expect("temp path should be UTF-8"),
]);
let error = SyncConfig::try_from(cli).expect_err("missing source must be rejected");
assert!(matches!(error, FastSyncError::InvalidSource(path) if path == missing));
Ok(())
}
#[test]
fn cli_config_normalizes_runtime_options() -> std::result::Result<(), Box<dyn std::error::Error>>
{
let source = tempdir()?;
let target = tempdir()?;
let cli = Cli::parse_from([
"fastsync",
source.path().to_str().expect("temp path should be UTF-8"),
target.path().to_str().expect("temp path should be UTF-8"),
"--strict",
"--verify",
"all",
"--no-sync-metadata",
"--preserve-times",
"false",
"--preserve-permissions",
"true",
"--no-atomic-write",
"--threads",
"0",
"--queue-size",
"0",
"--max-errors",
"3",
"--stop-on-error",
"--output",
"json",
"--log-level",
"debug",
]);
let config = SyncConfig::try_from(cli)?;
assert_eq!(config.compare_mode, CompareMode::Strict);
assert_eq!(config.verify_mode, VerifyMode::All);
assert!(!config.sync_metadata);
assert_eq!(config.preserve_times, PreserveMode::False);
assert_eq!(config.preserve_permissions, PreserveMode::True);
assert!(!config.atomic_write);
assert_eq!(config.threads, 1);
assert_eq!(config.queue_size, 1);
assert_eq!(config.max_errors, 3);
assert!(config.stop_on_error);
assert_eq!(config.output, OutputMode::Json);
assert_eq!(config.log_level, LogLevel::Debug);
Ok(())
}
#[test]
fn syncs_file_metadata_requires_enabled_metadata_and_preserve_mode() {
let mut config = SyncConfig {
source: "source".into(),
target: "target".into(),
dry_run: false,
delete: false,
follow_symlinks: false,
compare_mode: CompareMode::Fast,
hash_algorithm: HashAlgorithm::Blake3,
verify_mode: VerifyMode::Changed,
sync_metadata: true,
preserve_times: PreserveMode::False,
preserve_permissions: PreserveMode::False,
atomic_write: true,
threads: 1,
queue_size: 1,
max_errors: 1,
stop_on_error: false,
output: OutputMode::Text,
log_level: LogLevel::Info,
filter: PathFilter::disabled(),
};
assert!(!config.syncs_file_metadata());
config.preserve_times = PreserveMode::Auto;
assert!(config.syncs_file_metadata());
config.sync_metadata = false;
assert!(!config.syncs_file_metadata());
}
}