fastsync 0.10.1

A fast, safe one-way directory synchronization tool for local folders and network transfers.
Documentation
use std::io::IsTerminal;
use std::process::ExitCode;

use fastsync::cli::Cli;
use fastsync::config::{LogLevel, OutputMode, SyncConfig};
use fastsync::i18n::{self, tr};
use fastsync::network::NetworkCommand;
use tracing_indicatif::IndicatifLayer;
use tracing_indicatif::filter::IndicatifFilter;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::layer::{Layer, SubscriberExt};
use tracing_subscriber::util::SubscriberInitExt;

fn main() -> ExitCode {
    let args: Vec<_> = std::env::args_os().collect();
    let language = Cli::detect_language(&args);
    i18n::set_language(language);

    if args.len() == 1 {
        let mut command = Cli::command(language);
        if let Err(error) = command.print_long_help() {
            eprintln!(
                "fastsync: {}: {error}",
                tr(language, "app.help_print_failed")
            );
            return ExitCode::from(1);
        }
        println!();
        return ExitCode::SUCCESS;
    }

    if is_network_command(&args) {
        if let Some(subcommand) = empty_network_invocation(&args) {
            if let Err(error) = fastsync::network::print_subcommand_help(subcommand, language) {
                eprintln!(
                    "fastsync: {}: {error}",
                    tr(language, "app.help_print_failed")
                );
                return ExitCode::from(1);
            }
            println!();
            return ExitCode::SUCCESS;
        }

        let command = NetworkCommand::parse_from(args, language);
        return match command {
            NetworkCommand::Share(config) => {
                i18n::set_language(config.language);
                let progress = should_enable_terminal_progress();
                init_tracing_level(config.log_level, progress);
                match fastsync::network::run_share_with_progress(config, progress) {
                    Ok(()) => ExitCode::SUCCESS,
                    Err(error) => {
                        eprintln!("fastsync: {error}");
                        ExitCode::from(1)
                    }
                }
            }
            NetworkCommand::Connect(config) => {
                i18n::set_language(config.language);
                let progress = should_enable_terminal_progress();
                init_tracing_level(config.log_level, progress);
                match fastsync::network::run_connect_with_progress(config, progress) {
                    Ok(()) => ExitCode::SUCCESS,
                    Err(error) => {
                        eprintln!("fastsync: {error}");
                        ExitCode::from(1)
                    }
                }
            }
        };
    }

    let cli = Cli::parse_from(args);
    i18n::set_language(cli.language);
    let progress = should_enable_progress(cli.output);
    init_tracing_level(cli.log_level, progress);

    let output = cli.output;
    let language = cli.language;
    let config = match SyncConfig::try_from(cli) {
        Ok(config) => config,
        Err(error) => {
            eprintln!("fastsync: {error}");
            return ExitCode::from(2);
        }
    };

    match fastsync::run_sync_with_progress(config, progress) {
        Ok(summary) => {
            match output {
                OutputMode::Text => {
                    let use_color =
                        std::io::stdout().is_terminal() && std::env::var_os("NO_COLOR").is_none();
                    println!("{}", summary.to_text_with_language(language, use_color));
                }
                OutputMode::Json => match serde_json::to_string_pretty(&summary) {
                    Ok(json) => println!("{json}"),
                    Err(error) => {
                        eprintln!(
                            "fastsync: {}: {error}",
                            tr(language, "app.json_summary_failed")
                        );
                        return ExitCode::from(1);
                    }
                },
            }
            ExitCode::SUCCESS
        }
        Err(error) => {
            eprintln!("fastsync: {error}");
            ExitCode::from(1)
        }
    }
}

fn is_network_command(args: &[std::ffi::OsString]) -> bool {
    args.get(1)
        .and_then(|arg| arg.to_str())
        .is_some_and(|arg| matches!(arg, "share" | "s" | "connect" | "c"))
}

fn empty_network_invocation(args: &[std::ffi::OsString]) -> Option<&'static str> {
    if args.len() != 2 {
        return None;
    }

    match args.get(1).and_then(|arg| arg.to_str()) {
        Some("share" | "s") => Some("share"),
        Some("connect" | "c") => Some("connect"),
        _ => None,
    }
}

fn should_enable_progress(output: OutputMode) -> bool {
    output == OutputMode::Text && should_enable_terminal_progress()
}

fn should_enable_terminal_progress() -> bool {
    std::io::stderr().is_terminal()
        && std::env::var_os("NO_COLOR").is_none()
        && std::env::var_os("TERM").is_none_or(|term| term != "dumb")
}

fn init_tracing_level(log_level: LogLevel, progress: bool) {
    let filter = EnvFilter::new(log_level.as_str());
    if progress {
        let indicatif_layer = IndicatifLayer::new();
        let stderr_writer = indicatif_layer.get_stderr_writer();
        let indicatif_layer = indicatif_layer.with_filter(IndicatifFilter::new(false));
        let fmt_layer = tracing_subscriber::fmt::layer()
            .with_writer(stderr_writer)
            .with_target(false)
            .with_filter(filter);

        tracing_subscriber::registry()
            .with(fmt_layer)
            .with(indicatif_layer)
            .init();
    } else {
        let fmt_layer = tracing_subscriber::fmt::layer()
            .with_writer(std::io::stderr)
            .with_target(false)
            .with_filter(filter);

        tracing_subscriber::registry().with(fmt_layer).init();
    }
}

#[cfg(test)]
mod tests {
    use std::ffi::OsString;

    use super::*;

    #[test]
    fn detects_network_subcommands_and_aliases() {
        for command in ["share", "s", "connect", "c"] {
            let args = vec![OsString::from("fastsync"), OsString::from(command)];

            assert!(is_network_command(&args));
        }

        let local_args = vec![
            OsString::from("fastsync"),
            OsString::from("source"),
            OsString::from("target"),
        ];
        assert!(!is_network_command(&local_args));
    }

    #[test]
    fn empty_network_invocation_maps_aliases_to_canonical_help() {
        assert_eq!(
            empty_network_invocation(&[OsString::from("fastsync"), OsString::from("share")]),
            Some("share")
        );
        assert_eq!(
            empty_network_invocation(&[OsString::from("fastsync"), OsString::from("s")]),
            Some("share")
        );
        assert_eq!(
            empty_network_invocation(&[OsString::from("fastsync"), OsString::from("connect")]),
            Some("connect")
        );
        assert_eq!(
            empty_network_invocation(&[OsString::from("fastsync"), OsString::from("c")]),
            Some("connect")
        );
    }

    #[test]
    fn non_empty_network_invocation_is_not_help_fallback() {
        let args = [
            OsString::from("fastsync"),
            OsString::from("share"),
            OsString::from("/tmp/share"),
        ];

        assert_eq!(empty_network_invocation(&args), None);
    }
}