From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 78518E53F for ; Fri, 9 Dec 2022 12:14:34 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 550BE23211 for ; Fri, 9 Dec 2022 12:14:34 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Fri, 9 Dec 2022 12:14:32 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 4C29A443F5 for ; Fri, 9 Dec 2022 12:14:32 +0100 (CET) From: Lukas Wagner To: pbs-devel@lists.proxmox.com Date: Fri, 9 Dec 2022 12:14:25 +0100 Message-Id: <20221209111426.166003-4-l.wagner@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20221209111426.166003-1-l.wagner@proxmox.com> References: <20221209111426.166003-1-l.wagner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -2.361 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_SOMETLD_ARE_BAD_TLD 5 .bar, .beauty, .buzz, .cam, .casa, .cfd, .club, .date, .guru, .link, .live, .online, .press, .pw, .quest, .rest, .sbs, .shop, .stream, .top, .trade, .work, .xyz TLD abuse SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record T_PDS_OTHER_BAD_TLD 0.01 Untrustworthy TLDs Subject: [pbs-devel] [PATCH v4 proxmox-backup 3/4] debug cli: add colored output for `diff archive` X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Fri, 09 Dec 2022 11:14:34 -0000 This commit adds the `--color` flag to the `diff archive` tool. Valid values are `always`, `auto` and `never`. `always` and `never` should be self-explanatory, whereas `auto` will enable colors unless one of the following is true: - STDOUT is not a tty - TERM=dumb is set - NO_COLOR is set The tool will highlight changed file attributes in yellow. Furthermore, (A)dded files are highlighted in green, (M)odified in yellow and (D)eleted in red. Signed-off-by: Lukas Wagner --- Cargo.toml | 1 + src/bin/proxmox_backup_debug/diff.rs | 371 ++++++++++++++++++++++----- 2 files changed, 306 insertions(+), 66 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8ee48127..7fac1bfa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" siphasher = "0.3" syslog = "4.0" +termcolor = "1.1.2" tokio = { version = "1.6", features = [ "fs", "io-util", "io-std", "macros", "net", "parking_lot", "process", "rt", "rt-multi-thread", "signal", "time" ] } tokio-openssl = "0.6.1" tokio-stream = "0.1.0" diff --git a/src/bin/proxmox_backup_debug/diff.rs b/src/bin/proxmox_backup_debug/diff.rs index 06667fe1..2f621f8d 100644 --- a/src/bin/proxmox_backup_debug/diff.rs +++ b/src/bin/proxmox_backup_debug/diff.rs @@ -1,5 +1,6 @@ use std::collections::{HashMap, HashSet}; use std::ffi::{OsStr, OsString}; +use std::io::Write; use std::iter::FromIterator; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -28,6 +29,8 @@ use pbs_tools::json::required_string_param; use pxar::accessor::ReadAt; use pxar::EntryKind; use serde_json::Value; + +use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; use tokio::io::AsyncReadExt; type ChunkDigest = [u8; 32]; @@ -87,6 +90,11 @@ pub fn diff_commands() -> CommandLineInterface { type: bool, description: "Compare file content rather than solely relying on mtime for detecting modified files.", }, + "color": { + optional: true, + type: String, + description: "Set mode for colored output. Can be `always`, `auto` or `never`. `auto` will display colors only if stdout is a tty. Defaults to `auto`." + } } } )] @@ -106,6 +114,12 @@ async fn diff_archive_cmd(param: Value) -> Result<(), Error> { None => false, }; + let color = match param.get("color") { + Some(Value::String(color)) => color.as_str().try_into()?, + Some(_) => bail!("invalid color parameter. Valid choices are `always`, `auto` and `never`"), + None => ColorMode::Auto, + }; + let namespace = match param.get("ns") { Some(Value::String(ns)) => ns.parse()?, Some(_) => bail!("invalid namespace parameter"), @@ -133,6 +147,8 @@ async fn diff_archive_cmd(param: Value) -> Result<(), Error> { namespace, }; + let output_params = OutputParams { color }; + if archive_name.ends_with(".pxar") { let file_name = format!("{}.didx", archive_name); diff_archive( @@ -141,6 +157,7 @@ async fn diff_archive_cmd(param: Value) -> Result<(), Error> { &file_name, &repo_params, compare_contents, + &output_params, ) .await?; } else { @@ -156,6 +173,7 @@ async fn diff_archive( file_name: &str, repo_params: &RepoParams, compare_contents: bool, + output_params: &OutputParams, ) -> Result<(), Error> { let (index_a, accessor_a) = open_dynamic_index(snapshot_a, file_name, repo_params).await?; let (index_b, accessor_b) = open_dynamic_index(snapshot_b, file_name, repo_params).await?; @@ -209,17 +227,40 @@ async fn diff_archive( // which where *really* modified. let modified_files = compare_files(potentially_modified, compare_contents).await?; - show_file_list(&added_files, &deleted_files, &modified_files); + show_file_list(&added_files, &deleted_files, &modified_files, output_params)?; Ok(()) } +enum ColorMode { + Always, + Auto, + Never, +} + +impl TryFrom<&str> for ColorMode { + type Error = Error; + + fn try_from(value: &str) -> Result { + match value { + "auto" => Ok(Self::Auto), + "always" => Ok(Self::Always), + "never" => Ok(Self::Never), + _ => bail!("invalid color parameter. Valid choices are `always`, `auto` and `never`"), + } + } +} + struct RepoParams { repo: BackupRepository, crypt_config: Option>, namespace: BackupNamespace, } +struct OutputParams { + color: ColorMode, +} + async fn open_dynamic_index( snapshot: &str, archive_name: &str, @@ -533,70 +574,271 @@ impl ChangedProperties { } } -fn change_indicator(changed: bool) -> &'static str { - if changed { - "*" - } else { - " " - } +enum FileOperation { + Added, + Modified, + Deleted, } -fn format_filesize(entry: &FileEntry, changed: bool) -> String { - if let Some(size) = entry.file_size() { - format!( - "{}{:.1}", - change_indicator(changed), - HumanByte::new_decimal(size as f64) - ) - } else { - String::new() +struct ColumnWidths { + operation: usize, + entry_type: usize, + uid: usize, + gid: usize, + mode: usize, + filesize: usize, + mtime: usize, +} + +impl Default for ColumnWidths { + fn default() -> Self { + Self { + operation: 1, + entry_type: 2, + uid: 6, + gid: 6, + mode: 6, + filesize: 10, + mtime: 11, + } } } -fn format_mtime(entry: &FileEntry, changed: bool) -> String { - let mtime = &entry.metadata().stat.mtime; +struct FileEntryPrinter { + stream: StandardStream, + column_widths: ColumnWidths, + changed_color: Color, +} - let mut format = change_indicator(changed).to_owned(); - format.push_str("%F %T"); +impl FileEntryPrinter { + pub fn new(output_params: &OutputParams) -> Self { + let color_choice = match output_params.color { + ColorMode::Always => ColorChoice::Always, + ColorMode::Auto => { + if unsafe { libc::isatty(1) == 1 } { + // Show colors unless `TERM=dumb` or `NO_COLOR` is set. + ColorChoice::Auto + } else { + ColorChoice::Never + } + } + ColorMode::Never => ColorChoice::Never, + }; - proxmox_time::strftime_local(&format, mtime.secs).unwrap_or_default() -} + let stdout = StandardStream::stdout(color_choice); -fn format_mode(entry: &FileEntry, changed: bool) -> String { - let mode = entry.metadata().stat.mode & 0o7777; - format!("{}{:o}", change_indicator(changed), mode) -} + Self { + stream: stdout, + column_widths: ColumnWidths::default(), + changed_color: Color::Yellow, + } + } -fn format_entry_type(entry: &FileEntry, changed: bool) -> String { - let kind = match entry.kind() { - EntryKind::Symlink(_) => "l", - EntryKind::Hardlink(_) => "h", - EntryKind::Device(_) if entry.metadata().stat.is_blockdev() => "b", - EntryKind::Device(_) => "c", - EntryKind::Socket => "s", - EntryKind::Fifo => "p", - EntryKind::File { .. } => "f", - EntryKind::Directory => "d", - _ => " ", - }; + fn change_indicator(&self, changed: bool) -> &'static str { + if changed { + "*" + } else { + " " + } + } - format!("{}{}", change_indicator(changed), kind) -} + fn set_color_if_changed(&mut self, changed: bool) -> Result<(), Error> { + if changed { + self.stream + .set_color(ColorSpec::new().set_fg(Some(self.changed_color)))?; + } -fn format_uid(entry: &FileEntry, changed: bool) -> String { - format!("{}{}", change_indicator(changed), entry.metadata().stat.uid) -} + Ok(()) + } -fn format_gid(entry: &FileEntry, changed: bool) -> String { - format!("{}{}", change_indicator(changed), entry.metadata().stat.gid) -} + fn write_operation(&mut self, op: FileOperation) -> Result<(), Error> { + let (text, color) = match op { + FileOperation::Added => ("A", Color::Green), + FileOperation::Modified => ("M", Color::Yellow), + FileOperation::Deleted => ("D", Color::Red), + }; -fn format_file_name(entry: &FileEntry, changed: bool) -> String { - format!( - "{}{}", - change_indicator(changed), - entry.file_name().to_string_lossy() - ) + self.stream + .set_color(ColorSpec::new().set_fg(Some(color)))?; + + write!( + self.stream, + "{text:>width$}", + width = self.column_widths.operation, + )?; + + self.stream.reset()?; + + Ok(()) + } + + fn write_filesize(&mut self, entry: &FileEntry, changed: bool) -> Result<(), Error> { + let output = if let Some(size) = entry.file_size() { + format!( + "{}{:.1}", + self.change_indicator(changed), + HumanByte::new_decimal(size as f64) + ) + } else { + String::new() + }; + + self.set_color_if_changed(changed)?; + write!( + self.stream, + "{output:>width$}", + width = self.column_widths.filesize, + )?; + self.stream.reset()?; + + Ok(()) + } + + fn write_mtime(&mut self, entry: &FileEntry, changed: bool) -> Result<(), Error> { + let mtime = &entry.metadata().stat.mtime; + + let mut format = self.change_indicator(changed).to_owned(); + format.push_str("%F %T"); + + let output = proxmox_time::strftime_local(&format, mtime.secs).unwrap_or_default(); + + self.set_color_if_changed(changed)?; + write!( + self.stream, + "{output:>width$}", + width = self.column_widths.mtime, + )?; + self.stream.reset()?; + + Ok(()) + } + + fn write_mode(&mut self, entry: &FileEntry, changed: bool) -> Result<(), Error> { + let mode = entry.metadata().stat.mode & 0o7777; + let output = format!("{}{:o}", self.change_indicator(changed), mode); + + self.set_color_if_changed(changed)?; + write!( + self.stream, + "{output:>width$}", + width = self.column_widths.mode, + )?; + self.stream.reset()?; + + Ok(()) + } + + fn write_entry_type(&mut self, entry: &FileEntry, changed: bool) -> Result<(), Error> { + let kind = match entry.kind() { + EntryKind::Symlink(_) => "l", + EntryKind::Hardlink(_) => "h", + EntryKind::Device(_) if entry.metadata().stat.is_blockdev() => "b", + EntryKind::Device(_) => "c", + EntryKind::Socket => "s", + EntryKind::Fifo => "p", + EntryKind::File { .. } => "f", + EntryKind::Directory => "d", + _ => " ", + }; + + let output = format!("{}{}", self.change_indicator(changed), kind); + + self.set_color_if_changed(changed)?; + write!( + self.stream, + "{output:>width$}", + width = self.column_widths.entry_type, + )?; + self.stream.reset()?; + + Ok(()) + } + + fn write_uid(&mut self, entry: &FileEntry, changed: bool) -> Result<(), Error> { + let output = format!( + "{}{}", + self.change_indicator(changed), + entry.metadata().stat.uid + ); + + self.set_color_if_changed(changed)?; + write!( + self.stream, + "{output:>width$}", + width = self.column_widths.uid, + )?; + self.stream.reset()?; + Ok(()) + } + + fn write_gid(&mut self, entry: &FileEntry, changed: bool) -> Result<(), Error> { + let output = format!( + "{}{}", + self.change_indicator(changed), + entry.metadata().stat.gid + ); + + self.set_color_if_changed(changed)?; + write!( + self.stream, + "{output:>width$}", + width = self.column_widths.gid, + )?; + self.stream.reset()?; + Ok(()) + } + + fn write_file_name(&mut self, entry: &FileEntry, changed: bool) -> Result<(), Error> { + self.set_color_if_changed(changed)?; + write!( + self.stream, + "{}{}", + self.change_indicator(changed), + entry.file_name().to_string_lossy() + )?; + self.stream.reset()?; + + Ok(()) + } + + fn write_column_seperator(&mut self) -> Result<(), Error> { + write!(self.stream, " ")?; + Ok(()) + } + + /// Print a file entry, including `changed` indicators and column seperators + pub fn print_file_entry( + &mut self, + entry: &FileEntry, + changed: &ChangedProperties, + operation: FileOperation, + ) -> Result<(), Error> { + self.write_operation(operation)?; + self.write_column_seperator()?; + + self.write_entry_type(entry, changed.entry_type)?; + self.write_column_seperator()?; + + self.write_uid(entry, changed.uid)?; + self.write_column_seperator()?; + + self.write_gid(entry, changed.gid)?; + self.write_column_seperator()?; + + self.write_mode(entry, changed.mode)?; + self.write_column_seperator()?; + + self.write_filesize(entry, changed.size)?; + self.write_column_seperator()?; + + self.write_mtime(entry, changed.mtime)?; + self.write_column_seperator()?; + + self.write_file_name(entry, changed.content)?; + writeln!(self.stream)?; + + Ok(()) + } } /// Display a sorted list of added, modified, deleted files. @@ -604,7 +846,8 @@ fn show_file_list( added: &HashMap<&OsStr, &FileEntry>, deleted: &HashMap<&OsStr, &FileEntry>, modified: &HashMap<&OsStr, (&FileEntry, ChangedProperties)>, -) { + output_params: &OutputParams, +) -> Result<(), Error> { let mut all: Vec<&OsStr> = Vec::new(); all.extend(added.keys()); @@ -613,27 +856,23 @@ fn show_file_list( all.sort(); + let mut printer = FileEntryPrinter::new(output_params); + for file in all { - let (op, entry, changed) = if let Some(entry) = added.get(file) { - ("A", entry, ChangedProperties::default()) + let (operation, entry, changed) = if let Some(entry) = added.get(file) { + (FileOperation::Added, entry, ChangedProperties::default()) } else if let Some(entry) = deleted.get(file) { - ("D", entry, ChangedProperties::default()) + (FileOperation::Deleted, entry, ChangedProperties::default()) } else if let Some((entry, changed)) = modified.get(file) { - ("M", entry, *changed) + (FileOperation::Modified, entry, *changed) } else { unreachable!(); }; - let entry_type = format_entry_type(entry, changed.entry_type); - let uid = format_uid(entry, changed.uid); - let gid = format_gid(entry, changed.gid); - let mode = format_mode(entry, changed.mode); - let size = format_filesize(entry, changed.size); - let mtime = format_mtime(entry, changed.mtime); - let name = format_file_name(entry, changed.content); - - println!("{op} {entry_type:>2} {mode:>5} {uid:>6} {gid:>6} {size:>10} {mtime:11} {name}"); + printer.print_file_entry(entry, &changed, operation)?; } + + Ok(()) } #[cfg(test)] -- 2.30.2