1mod config;
4
5use anyhow::{Context, Result};
6use clap::Parser;
7use copyrust::{Cancellation, CopyEngine, Progress};
8use std::{
9 ffi::OsString,
10 io::{IsTerminal, Write},
11 path::PathBuf,
12};
13
14#[derive(Parser, Debug)]
16#[command(name = "copyrust", version, about, arg_required_else_help = true)]
17pub struct Cli {
18 #[arg(long, num_args = 2, value_names = ["SOURCE", "DESTINATION"], action = clap::ArgAction::Append)]
20 pair: Vec<PathBuf>,
21 #[arg(long)]
23 config: Option<PathBuf>,
24 #[arg(long, value_parser = ["mtime", "hash", "auto", "always"])]
26 compare: Option<String>,
27 #[arg(long, value_parser = ["xxh3", "xxh64", "blake3"])]
29 hash_algorithm: Option<String>,
30 #[arg(long)]
32 exclude_dir: Vec<String>,
33 #[arg(long)]
35 exclude_file: Vec<String>,
36 #[arg(long)]
38 include_file: Vec<String>,
39 #[arg(long, conflicts_with = "no_recursive")]
41 recursive: bool,
42 #[arg(long)]
44 no_recursive: bool,
45 #[arg(long, conflicts_with = "no_preserve_mtime")]
47 preserve_mtime: bool,
48 #[arg(long)]
50 no_preserve_mtime: bool,
51 #[arg(long, conflicts_with = "no_atomic")]
53 atomic: bool,
54 #[arg(long)]
56 no_atomic: bool,
57 #[arg(long)]
59 case_sensitive: bool,
60 #[arg(long)]
62 purge: bool,
63 #[arg(long)]
65 dry_run: bool,
66 #[arg(long)]
68 scan_workers: Option<usize>,
69 #[arg(long)]
71 hash_workers: Option<usize>,
72 #[arg(long)]
74 copy_workers: Option<usize>,
75 #[arg(long)]
77 retries: Option<usize>,
78 #[arg(long)]
80 progress_interval: Option<String>,
81 #[arg(long, default_value = "auto", value_parser = ["auto", "plain", "json", "off"])]
83 progress: String,
84}
85
86impl Cli {
87 pub fn run(args: impl IntoIterator<Item = impl Into<OsString> + Clone>) -> i32 {
89 let subscriber = tracing_subscriber::fmt()
90 .with_target(false)
91 .with_writer(std::io::stderr)
92 .with_ansi(false)
93 .finish();
94 tracing::subscriber::with_default(subscriber, || {
95 let cli = match Self::try_parse_from(args) {
96 Ok(cli) => cli,
97 Err(error) => {
98 let code = error.exit_code();
99 if let Err(print_error) = error.print() {
100 tracing::error!("Failed to print command help: {print_error}");
101 }
102 return code;
103 }
104 };
105 match cli.execute() {
106 Ok(true) => 0,
107 Ok(false) => 1,
108 Err(error) => {
109 tracing::error!("{error:#}");
110 1
111 }
112 }
113 })
114 }
115
116 fn execute(self) -> Result<bool> {
118 let (jobs, options) = config::Configuration::load(&self)?;
119 let cancellation = Cancellation::default();
120 let signal = cancellation.clone();
121 ctrlc::set_handler(move || signal.cancel())
122 .context("Failed to install cancellation handler")?;
123 let summary = CopyEngine::run(&jobs, &options, cancellation, |event| self.render(event))?;
124 for error in &summary.errors {
125 tracing::error!("{error}");
126 }
127 Ok(summary.success())
128 }
129
130 fn render(&self, event: &Progress) -> Result<()> {
132 if self.progress == "json" {
133 let mut output = std::io::stdout().lock();
134 serde_json::to_writer(&mut output, event).context("Failed to serialize progress")?;
135 writeln!(output).context("Failed to write progress event")?;
136 } else if self.progress != "off" || event.complete || event.phase == "failed" {
137 let subscriber = tracing_subscriber::fmt()
138 .with_target(false)
139 .with_writer(std::io::stderr)
140 .with_ansi(self.progress == "auto" && std::io::stderr().is_terminal())
141 .finish();
142 tracing::subscriber::with_default(subscriber, || {
143 tracing::info!(
144 phase = %event.phase, scanned = event.scanned_files, compared = event.compared_files,
145 copied_files = event.copied_files, copied_bytes = event.copied_bytes,
146 hashed_bytes = event.hashed_bytes, skipped_mtime = event.skipped_mtime,
147 skipped_hash = event.skipped_hash, deleted = event.deleted_files,
148 total_files = ?event.total_files, total_copy_bytes = ?event.total_copy_bytes,
149 planned_copy_files = event.planned_copy_files, planned_delete_files = event.planned_delete_files,
150 average_mib_per_second = format_args!("{:.2}", event.copied_bytes as f64 / event.elapsed_seconds.max(0.001) / 1048576.0),
151 failed = event.failed_files, elapsed_seconds = format_args!("{:.3}", event.elapsed_seconds),
152 "copy progress"
153 )
154 });
155 }
156 Ok(())
157 }
158}