From 9f1ddb041826b663b54cfe69a14257d98d097708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20O=C5=BEana?= Date: Wed, 17 Jun 2026 09:04:25 +0200 Subject: [PATCH] Simplify logging: drop indicatif, use set-once global verbosity Remove the unused `indicatif` dependency (declared but used nowhere). Replace the threaded `verbosity` parameter with a process-global atomic set once in `main` and read by `log()`. This drops the parameter from `deploy_mutations`, `execute_uploads`, and `auto_init` (and the now- unneeded too_many_arguments allow). Logging behavior is unchanged: stderr/stdout split, level filtering, and quiet suppression all preserved. Closes #2 --- Cargo.lock | 70 ----------------------------------------------- Cargo.toml | 1 - src/log.rs | 35 ++++++++++++++++++++++++ src/main.rs | 2 ++ src/sync.rs | 79 +++++++++-------------------------------------------- 5 files changed, 50 insertions(+), 137 deletions(-) create mode 100644 src/log.rs diff --git a/Cargo.lock b/Cargo.lock index fdd342d..834b4d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -380,19 +380,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "console" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "unicode-width", - "windows-sys 0.59.0", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -459,12 +446,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - [[package]] name = "errno" version = "0.3.14" @@ -531,7 +512,6 @@ dependencies = [ "futures-rustls", "globset", "ignore", - "indicatif", "serde", "serde_json", "sha2", @@ -764,19 +744,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "indicatif" -version = "0.17.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" -dependencies = [ - "console", - "number_prefix", - "portable-atomic", - "unicode-width", - "web-time", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -899,12 +866,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "once_cell" version = "1.21.4" @@ -1003,12 +964,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - [[package]] name = "proc-macro2" version = "1.0.106" @@ -1348,12 +1303,6 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - [[package]] name = "untrusted" version = "0.9.0" @@ -1458,16 +1407,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "webpki-roots" version = "0.26.11" @@ -1563,15 +1502,6 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-sys" version = "0.61.2" diff --git a/Cargo.toml b/Cargo.toml index 3bc2b61..5449290 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,6 @@ clap = { version = "4", features = ["derive", "env"] } anyhow = "1" thiserror = "2" chrono = { version = "0.4", features = ["serde"] } -indicatif = "0.17" webpki-roots = "0.26" futures = "0.3" futures-rustls = "0.26" diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..341e8f4 --- /dev/null +++ b/src/log.rs @@ -0,0 +1,35 @@ +//! Process-global verbosity + leveled stderr logging. +//! +//! Verbosity is set once in `main` and read by `log()`, which keeps it out of +//! every sync function signature. The level lives in an atomic so the parallel +//! upload workers can call `log()` concurrently and safely (`eprintln!` itself +//! already locks stderr). + +use crate::config::Verbosity; +use std::sync::atomic::{AtomicU8, Ordering}; + +fn rank(v: Verbosity) -> u8 { + match v { + Verbosity::Quiet => 0, + Verbosity::Normal => 1, + Verbosity::Verbose => 2, + } +} + +static CURRENT: AtomicU8 = AtomicU8::new(1); // Normal until set. + +/// Set the global verbosity. Call once, early, from `main`. +pub fn set_verbosity(v: Verbosity) { + CURRENT.store(rank(v), Ordering::Relaxed); +} + +/// Print `msg` to stderr if the current verbosity is at least `level`. +/// +/// Status/progress logs go to stderr; the dry-run plan goes to stdout via +/// `println!` in `sync`. Quiet (rank 0) suppresses every status log — errors +/// propagate via `Result` instead. +pub fn log(level: Verbosity, msg: &str) { + if CURRENT.load(Ordering::Relaxed) >= rank(level) { + eprintln!("{msg}"); + } +} diff --git a/src/main.rs b/src/main.rs index 9bfc53d..56c465e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ mod config; mod error; mod hasher; mod ignore; +mod log; mod state; mod sync; mod walker; @@ -17,6 +18,7 @@ mod walker; async fn main() -> Result<()> { let args = cli::Args::parse(); let cfg = config::Config::from_args(args)?; + log::set_verbosity(cfg.verbosity); sync::run(cfg).await?; Ok(()) } diff --git a/src/sync.rs b/src/sync.rs index a657329..9f44a6d 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -4,6 +4,7 @@ use crate::client::Client; use crate::config::{Config, Verbosity}; use crate::error::{FtpSyncError, Result}; use crate::hasher; +use crate::log::log; use crate::state::State; use crate::walker::{self, LocalFile}; use std::collections::HashMap; @@ -19,12 +20,9 @@ struct Plan { /// Top-level entry point invoked from main. pub async fn run(cfg: Config) -> Result<()> { - let verbosity = cfg.verbosity; - // 1. Discover + hash local files. let local = walker::discover(&cfg)?; log( - verbosity, Verbosity::Verbose, &format!("Discovered {} local files", local.len()), ); @@ -38,7 +36,6 @@ pub async fn run(cfg: Config) -> Result<()> { // 2. Connect + fetch (or initialize) state. log( - verbosity, Verbosity::Normal, &format!("Connecting to {}:{}", cfg.server, cfg.port), ); @@ -47,20 +44,18 @@ pub async fn run(cfg: Config) -> Result<()> { let state_path = cfg.remote_path(&cfg.state_file); let state = match client.download(&state_path).await { Ok(bytes) => { - log(verbosity, Verbosity::Verbose, "Loaded existing state file"); + log(Verbosity::Verbose, "Loaded existing state file"); State::from_bytes(&bytes)? } Err(FtpSyncError::NotFound(_)) if cfg.auto_init => { log( - verbosity, Verbosity::Normal, "No state file found — auto-initializing from server (this can be slow)", ); - auto_init(&mut client, &cfg, verbosity).await? + auto_init(&mut client, &cfg).await? } Err(FtpSyncError::NotFound(_)) => { log( - verbosity, Verbosity::Normal, "No state file found — treating server as empty", ); @@ -72,7 +67,6 @@ pub async fn run(cfg: Config) -> Result<()> { // 3. Diff. let plan = diff(&local, &local_hashes, &state, cfg.no_delete); log( - verbosity, Verbosity::Normal, &format!( "{} to upload, {} to delete", @@ -94,11 +88,7 @@ pub async fn run(cfg: Config) -> Result<()> { } if plan.to_upload.is_empty() && plan.to_delete.is_empty() && cfg.purge.is_empty() { - log( - verbosity, - Verbosity::Normal, - "Nothing to do — server is up to date", - ); + log(Verbosity::Normal, "Nothing to do — server is up to date"); client.quit().await?; return Ok(()); } @@ -109,7 +99,6 @@ pub async fn run(cfg: Config) -> Result<()> { let lock_path = cfg.remote_path(&format!("{}.running", cfg.state_file)); if client.exists(&lock_path).await? { log( - verbosity, Verbosity::Normal, &format!( "warning: {lock_path} already exists — a previous deploy may have been \ @@ -122,16 +111,8 @@ pub async fn run(cfg: Config) -> Result<()> { .await?; // 6. Mutate the server, then always release the lock (even on error). - let result = deploy_mutations( - &mut client, - &cfg, - &plan, - &local_hashes, - state, - verbosity, - &state_path, - ) - .await; + let result = + deploy_mutations(&mut client, &cfg, &plan, &local_hashes, state, &state_path).await; let _ = client.delete(&lock_path).await; result?; @@ -140,34 +121,25 @@ pub async fn run(cfg: Config) -> Result<()> { } /// Run the mutating phase: deletes, parallel uploads, purge, and state commit. -#[allow(clippy::too_many_arguments)] async fn deploy_mutations( client: &mut Client, cfg: &Config, plan: &Plan, local_hashes: &HashMap, mut state: State, - verbosity: Verbosity, state_path: &str, ) -> Result<()> { // Deletes on the primary connection. for path in &plan.to_delete { let remote = cfg.remote_path(path); - log(verbosity, Verbosity::Verbose, &format!("DELETE {path}")); + log(Verbosity::Verbose, &format!("DELETE {path}")); client.delete(&remote).await?; state.remove(path); } // Uploads (parallel via connection pool). let state = Arc::new(Mutex::new(state)); - execute_uploads( - cfg, - &plan.to_upload, - local_hashes, - Arc::clone(&state), - verbosity, - ) - .await?; + execute_uploads(cfg, &plan.to_upload, local_hashes, Arc::clone(&state)).await?; let mut state = Arc::try_unwrap(state) .map_err(|_| FtpSyncError::Config("internal: state still shared".into()))? .into_inner(); @@ -175,14 +147,14 @@ async fn deploy_mutations( // Purge requested directories (e.g. caches) after the sync. for dir in &cfg.purge { let remote = cfg.remote_path(dir); - log(verbosity, Verbosity::Normal, &format!("Purging {dir}")); + log(Verbosity::Normal, &format!("Purging {dir}")); client.purge(&remote).await?; } // Commit state. let bytes = state.render_json()?; client.upload(state_path, &bytes).await?; - log(verbosity, Verbosity::Normal, "State committed"); + log(Verbosity::Normal, "State committed"); Ok(()) } @@ -215,7 +187,7 @@ fn diff( } /// Auto-init: hash every remote file under server-dir to bootstrap the state. -async fn auto_init(client: &mut Client, cfg: &Config, verbosity: Verbosity) -> Result { +async fn auto_init(client: &mut Client, cfg: &Config) -> Result { let mut state = State::empty(); let remote_files = client .list_recursive(&format!("/{}", cfg.server_dir)) @@ -225,11 +197,7 @@ async fn auto_init(client: &mut Client, cfg: &Config, verbosity: Verbosity) -> R continue; } let remote = cfg.remote_path(&rel); - log( - verbosity, - Verbosity::Verbose, - &format!("HASH (remote) {rel}"), - ); + log(Verbosity::Verbose, &format!("HASH (remote) {rel}")); let bytes = client.download(&remote).await?; let hash = hasher::hash_bytes(&bytes); state.set(&rel, hash, bytes.len() as u64); @@ -248,7 +216,6 @@ async fn execute_uploads( files: &[LocalFile], local_hashes: &HashMap, state: Arc>, - verbosity: Verbosity, ) -> Result<()> { if files.is_empty() { return Ok(()); @@ -283,11 +250,7 @@ async fn execute_uploads( .map_err(|e| FtpSyncError::Config(format!("join error: {e}")))??; let remote = cfg.remote_path(&file.rel_path); - log( - verbosity, - Verbosity::Normal, - &format!("UPLOAD {}", file.rel_path), - ); + log(Verbosity::Normal, &format!("UPLOAD {}", file.rel_path)); client.upload_atomic(&remote, &data).await?; if let Some((hash, size)) = hashes.get(&file.rel_path) { @@ -307,22 +270,6 @@ async fn execute_uploads( Ok(()) } -/// Print `msg` if the current verbosity is at least `level`. -fn log(current: Verbosity, level: Verbosity, msg: &str) { - let rank = |v: Verbosity| match v { - Verbosity::Quiet => 0, - Verbosity::Normal => 1, - Verbosity::Verbose => 2, - }; - // Quiet suppresses everything except errors (handled by `?`). - if current == Verbosity::Quiet { - return; - } - if rank(current) >= rank(level) { - eprintln!("{msg}"); - } -} - #[cfg(test)] mod tests { use super::*;