summary history branches tags files
commit:cef57a9034ee5d84440865c40b85b1ebeff35554
author:Trevor Bentley
committer:Trevor Bentley
date:Mon Jan 13 19:57:19 2025 +0100
parents:6bcb534854b504cd4198c5ecf9b1ebd141a5ffac
add dry run, halt cli flag, error code on failure
diff --git a/src/main.rs b/src/main.rs
line changes: +70/-18
index 1413bbd..8b73773
--- a/src/main.rs
+++ b/src/main.rs
@@ -12,12 +12,14 @@ use serde::{Serialize, Deserialize};
 enum PwwError {
     Unknown(String),
     MissingSidecar(String),
+    InvalidImage,
 }
 impl std::fmt::Display for PwwError {
     fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
         match self {
-            PwwError::Unknown(s) => write!(f, "PWW unknown error: {}", s),
-            PwwError::MissingSidecar(s) => write!(f, "PWW missing XMP sidecar error: {}", s),
+            PwwError::Unknown(s) => write!(f, "PWW error: {}", s),
+            PwwError::MissingSidecar(s) => write!(f, "missing XMP sidecar: {}", s),
+            PwwError::InvalidImage => write!(f, "input image invalid"),
         }
     }
 }
@@ -29,6 +31,11 @@ struct PwwArgs {
     #[arg(short = 'b', long)]
     identifier_bin: Option<PathBuf>,
 
+    /// Do not actually write metadata to file, but print which files
+    /// would have changed.
+    #[arg(short = 'n', long)]
+    dry_run: bool,
+
     /// Print information about which tags are written
     #[arg(short = 'v', long)]
     verbose: bool,
@@ -37,6 +44,10 @@ struct PwwArgs {
     #[arg(short = 'd', long)]
     debug: bool,
 
+    /// Whether to stop processing after first error, or continue.
+    #[arg(short = 'e', long)]
+    halt_on_error: bool,
+
     /// Keep converted image files in temp directory instead of
     /// removing them.
     #[arg(long)]
@@ -65,6 +76,7 @@ enum FileUpdatePolicy {
 
     /// Only update XMP sidecar (error if XMP file missing).  Only
     /// sidecar tags are used.
+    #[default]
     SidecarOnly,
 
     /// Only update image itself.  Only image tags are used.
@@ -82,7 +94,6 @@ enum FileUpdatePolicy {
     /// known types with good EXIF support (JPG, PNG, TIFF, WebP).
     /// Either sidecar or image tags are used.  Error if XMP file
     /// missing.
-    #[default]
     SidecarUnlessCommonImage,
 }
 
@@ -228,6 +239,20 @@ impl PwwConfig {
         }
     }
 
+    fn max_dimension(&self) -> u32 {
+        match self.max_dimension {
+            0 => std::u32::MAX,
+            _ => self.max_dimension
+        }
+    }
+
+    fn min_dimension(&self) -> u32 {
+        match self.max_dimension {
+            0 => std::u32::MAX,
+            _ => self.max_dimension
+        }
+    }
+
     fn image_paths(&self) -> Vec<PathBuf> {
         match &self.cli {
             Some(c) => c.image_paths.clone(),
@@ -235,6 +260,10 @@ impl PwwConfig {
         }
     }
 
+    fn dry_run(&self) -> bool {
+        self.cli.as_ref().map(|c| c.dry_run).unwrap_or_default()
+    }
+
     fn verbose(&self) -> bool {
         self.cli.as_ref().map(|c| c.verbose).unwrap_or_default()
     }
@@ -244,6 +273,10 @@ impl PwwConfig {
         self.cli.as_ref().map(|c| c.debug).unwrap_or_default()
     }
 
+    fn halt_on_error(&self) -> bool {
+        self.cli.as_ref().map(|c| c.halt_on_error).unwrap_or_default()
+    }
+
     fn keep_converted(&self) -> bool {
         self.cli.as_ref().map(|c| c.keep_converted).unwrap_or_default()
     }
@@ -298,8 +331,11 @@ fn exec_identifier<P: AsRef<Path>>(config: &PwwConfig, file_path: P) -> Result<S
         .stdout(Stdio::piped())
         .spawn()
         .map_err(|e| PwwError::Unknown(format!("Failed to launch identifier binary: {}", e)))?;
-    p.wait()
+    let res = p.wait()
         .map_err(|e| PwwError::Unknown(format!("Failed to wait for identifier binary: {}", e)))?;
+    if !res.success() {
+        return Err(PwwError::Unknown("identifier binary failed to execute.".into()));
+    }
     let mut s: String = String::new();
     let mut stdout = p.stdout.ok_or_else(|| PwwError::Unknown(format!("Failed to get output from identifier binary")))?;
     stdout.read_to_string(&mut s).map_err(|x| PwwError::Unknown(x.to_string()))?;
@@ -382,7 +418,7 @@ fn image_needs_conversion<P: AsRef<Path>>(config: &PwwConfig, filepath: P) -> Re
                             DownscalePolicy::Always |
                             DownscalePolicy::Large |
                             DownscalePolicy::LargeOrConverted => {
-                                (true, max_dimension <= config.max_dimension)
+                                (true, max_dimension <= config.max_dimension())
                             },
                         }
                     },
@@ -525,10 +561,18 @@ fn write_tags_to_file<P: AsRef<std::ffi::OsStr>>(config: &PwwConfig, metatags: &
             println!(" - updated tag: {}", metatag);
         }
     }
-    meta.save_to_file(&filepath)
-        .map_err(|e| PwwError::Unknown(format!("Failed to write metadata to image file: {}", e)))?;
-    if config.verbose() {
-        println!(" - saved: {}\n", filepath.as_ref().to_string_lossy());
+
+    match config.dry_run() {
+        false => {
+            meta.save_to_file(&filepath)
+                .map_err(|e| PwwError::Unknown(format!("Failed to write metadata to image file: {}", e)))?;
+            if config.verbose() {
+                println!(" - saved: {}\n", filepath.as_ref().to_string_lossy());
+            }
+        },
+        true => {
+            println!("would update: {} (dry run)\n", filepath.as_ref().to_string_lossy());
+        },
     }
     Ok(())
 }
@@ -552,6 +596,8 @@ fn process_image(config: &PwwConfig, input_path: &Path) -> Result<(), PwwError> 
             if config.debug() {
                 println!(" - converting to rgb8 jpg");
             }
+            let i = Image::<u8, image2::Rgb>::open(&input_path)
+                .map_err(|_e| PwwError::InvalidImage)?;
             let temp_dir = config.temp_dir.clone()
                 .unwrap_or(PathBuf::from(std::env::temp_dir()));
             tmpfile = Some(tempfile::Builder::new()
@@ -563,8 +609,6 @@ fn process_image(config: &PwwConfig, input_path: &Path) -> Result<(), PwwError> 
                            .map_err(|e| PwwError::Unknown(format!("Failed to create temp file for image conversion: {}", e)))?);
             let outpath = tmpfile.as_ref().map(|x| x.path().to_path_buf())
                 .ok_or_else(|| PwwError::Unknown(format!("Unable to open temp file for image conversion.")))?;
-            let i = Image::<u8, image2::Rgb>::open(&input_path)
-                .map_err(|e| PwwError::Unknown(format!("Unable to open source image file: {}", e)))?;
             // convert to Rgb8
             let conv = image2::filter::convert();
             let i: Image<u8, image2::Rgb> = i.run(conv, None);
@@ -581,16 +625,16 @@ fn process_image(config: &PwwConfig, input_path: &Path) -> Result<(), PwwError> 
                     true
                 }
                 DownscalePolicy::Large => {
-                    max_dimension as u32 > config.max_dimension
+                    max_dimension as u32 > config.max_dimension()
                 }
                 DownscalePolicy::Never => {
                     false
                 }
             };
-            if should_scale && min_dimension as u32 > config.min_dimension {
-                let scale = config.min_dimension as f64 / min_dimension as f64;
+            if should_scale && min_dimension as u32 > config.min_dimension() {
+                let scale = config.min_dimension() as f64 / min_dimension as f64;
                 if config.debug() {
-                    println!(" - scaling by {:.2} ({} to {})", scale, min_dimension, config.min_dimension);
+                    println!(" - scaling by {:.2} ({} to {})", scale, min_dimension, config.min_dimension());
                 }
                 i.scale(scale, scale);
             }
@@ -658,20 +702,28 @@ fn main() -> Result<(), PwwError> {
         .map_err(|e| PwwError::Unknown(format!("Error loading config file: {}", e)))?;
     config.cli = Some(PwwArgs::parse());
 
+    let mut count = 0;
     for input_path in config.image_paths() {
         match process_image(&config, &input_path) {
             Err(e) => {
                 println!("Error processing image ({}): {}", input_path.to_string_lossy(), e);
-                if config.halt_on_error {
-                    break;
+                if config.halt_on_error() {
+                    std::process::exit(1);
                 }
                 if config.verbose() || config.debug() {
                     println!();
                 }
             },
-            _ => {}
+            _ => {
+                count += 1;
+            }
         }
     }
 
+    if count == 0 {
+        println!("Error: no images processed.");
+        std::process::exit(1);
+    }
+
     Ok(())
 }