From 4bfd43e1e0c442b7c0ae0bf2c5b01bfbd642fcc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20P=C3=A9rex?= Date: Fri, 23 May 2025 19:22:44 +0100 Subject: [PATCH] fix: correct path handling in directory processing - Fix duplicate subdirectory creation bug when processing nested directories - Change relative path calculation to use current directory instead of root - Remove redundant subdirectory creation code - Improve path display in logs to show cleaner relative paths --- src/directory_processor.rs | 177 +++++++++++++++++++++++++++++++++++++ src/main.rs | 169 +++-------------------------------- src/tools.rs | 86 ++++++++++++++++++ 3 files changed, 275 insertions(+), 157 deletions(-) create mode 100644 src/directory_processor.rs create mode 100644 src/tools.rs diff --git a/src/directory_processor.rs b/src/directory_processor.rs new file mode 100644 index 0000000..57f44c5 --- /dev/null +++ b/src/directory_processor.rs @@ -0,0 +1,177 @@ +use std::collections::HashSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::UNIX_EPOCH; +use crate::tools; +use crate::FILES_TO_COPY; + +pub struct DirectoryProcessor { + source_dir: PathBuf, + target_dir: PathBuf, + source_files: HashSet, +} + +impl DirectoryProcessor { + pub fn new(source: PathBuf, target: PathBuf) -> Self { + Self { + source_dir: source, + target_dir: target, + source_files: HashSet::new(), + } + } + + fn needs_copy_or_conversion(source_path: &Path, dest_path: &Path) -> bool { + if !dest_path.exists() { + return true; + } + + let source_modified = fs::metadata(source_path) + .and_then(|m| m.modified()) + .unwrap_or(UNIX_EPOCH); + + let dest_modified = fs::metadata(dest_path) + .and_then(|m| m.modified()) + .unwrap_or(UNIX_EPOCH); + + source_modified > dest_modified + } + + fn get_source_files(&mut self, current_dir: &Path) -> Result<(), String> { + let entries = fs::read_dir(current_dir) + .map_err(|e| format!("Error reading directory {}: {}", current_dir.display(), e))?; + + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + self.get_source_files(&path)?; + } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + if ext == "odt" || FILES_TO_COPY.contains(&ext.to_lowercase().as_str()) { + if let Ok(rel_path) = path.strip_prefix(&self.source_dir) { + self.source_files.insert(rel_path.to_path_buf()); + } + } + } + } + Ok(()) + } + + fn clean_target_directory(&self, current_dir: &Path) -> Result { + let entries = fs::read_dir(current_dir) + .map_err(|e| format!("Error reading directory {}: {}", current_dir.display(), e))?; + + let mut is_empty = true; + + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + // Recursively check subdirectories + let subdir_empty = self.clean_target_directory(&path)?; + + if subdir_empty { + if let Err(e) = fs::remove_dir(&path) { + eprintln!("Warning: Could not remove empty directory {}: {}", path.display(), e); + } else { + println!("Removed empty directory: {}", path.display()); + } + } else { + is_empty = false; + } + } else { + let rel_path = path.strip_prefix(&self.target_dir) + .map_err(|e| format!("Error getting relative path for {}: {}", path.display(), e))?; + + // Check if this file should exist based on source files + let should_exist = if let Some(ext) = rel_path.extension().and_then(|e| e.to_str()) { + if ext == "pdf" { + // For PDF files, check if corresponding ODT exists in source + self.source_files.contains(&rel_path.with_extension("odt")) + } else { + // For other files, check if they exist in source + self.source_files.contains(rel_path) + } + } else { + false + }; + + if !should_exist { + if let Err(e) = fs::remove_file(&path) { + eprintln!("Warning: Could not remove file {}: {}", path.display(), e); + } else { + println!("Removed obsolete file: {}", path.display()); + } + } else { + is_empty = false; + } + } + } + + Ok(is_empty) + } + + fn process_directory(&self, current_source: &Path, current_target: &Path) -> Result<(), String> { + // Create destination directory if it doesn't exist + if let Err(e) = fs::create_dir_all(current_target) { + return Err(format!("Error creating destination directory: {}", e)); + } + + let entries = fs::read_dir(current_source) + .map_err(|e| format!("Error reading source directory {}: {}", current_source.display(), e))?; + + for entry in entries.flatten() { + let path = entry.path(); + + if path.is_dir() { + // Get the relative path from source to the current subdirectory + let relative_path = path.strip_prefix(&self.source_dir) + .map_err(|e| format!("Error getting relative path: {}", e))?; + + // Create the corresponding destination subdirectory + let dest_subdir = self.target_dir.join(relative_path); + + // Recursively process the subdirectory + self.process_directory(&path, &dest_subdir)?; + } else if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + // Get the relative path from source to the current file + let relative_path = path.strip_prefix(current_source) + .map_err(|e| format!("Error getting relative path: {}", e))?; + + if ext == "odt" { + // Construct the PDF path in the current target directory + let pdf_path = current_target.join( + relative_path.with_extension("pdf") + ); + + if Self::needs_copy_or_conversion(&path, &pdf_path) { + match tools::convert_file(&path, &pdf_path.parent().unwrap_or(current_target)) { + Ok(_) => println!("Converted: {} -> {}", path.strip_prefix(&self.source_dir).unwrap().display(), pdf_path.strip_prefix(&self.target_dir).unwrap().display()), + Err(e) => eprintln!("Error: {} - {}", path.strip_prefix(&self.source_dir).unwrap().display(), e), + } + } + } else if FILES_TO_COPY.contains(&ext.to_lowercase().as_str()) { + // For files to copy directly + let dest_path = current_target.join(relative_path); + if Self::needs_copy_or_conversion(&path, &dest_path) { + match fs::copy(&path, &dest_path) { + Ok(_) => println!("Copied file: {} -> {}", path.strip_prefix(&self.source_dir).unwrap().display(), dest_path.strip_prefix(&self.target_dir).unwrap().display()), + Err(e) => eprintln!("Error copying file: {} - {}", path.strip_prefix(&self.source_dir).unwrap().display(), e), + } + } + } + } + } + Ok(()) + } + + pub fn process(&mut self) -> Result<(), String> { + // Process all files + self.process_directory(&self.source_dir.to_owned(), &self.target_dir.to_owned())?; + + // Get list of source files for cleaning + self.get_source_files(&self.source_dir.to_owned())?; + + // Finally clean target directory + self.clean_target_directory(&self.target_dir.to_owned())?; + + Ok(()) + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index fbfa095..81a65fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,12 @@ use clap::Parser; -use std::fs; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::time::UNIX_EPOCH; -use std::env; -// use std::time::{SystemTime, UNIX_EPOCH}; +use std::path::PathBuf; + +mod tools; +mod directory_processor; + +use directory_processor::DirectoryProcessor; + +pub const FILES_TO_COPY: [&str; 10] = ["jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp", "avif", "txt", "md"]; #[derive(Parser, Debug)] #[command( @@ -20,159 +22,12 @@ struct Args { dest: PathBuf, } -fn needs_conversion(odt_path: &Path, pdf_path: &Path) -> bool { - if !pdf_path.exists() { - return true; - } - - let odt_modified = fs::metadata(odt_path) - .and_then(|m| m.modified()) - .unwrap_or(UNIX_EPOCH); - - let pdf_modified = fs::metadata(pdf_path) - .and_then(|m| m.modified()) - .unwrap_or(UNIX_EPOCH); - - odt_modified > pdf_modified -} - -fn get_soffice_path() -> Result { - // First try environment variable if set - if let Ok(path) = env::var("LIBREOFFICE_PATH") { - if Path::new(&path).exists() { - return Ok(path); - } - } - - // Check common installation paths - let paths = if cfg!(target_os = "macos") { - vec![ - "/Applications/LibreOffice.app/Contents/MacOS/soffice", - "/Applications/LibreOffice-still.app/Contents/MacOS/soffice", - "/Applications/LibreOffice-fresh.app/Contents/MacOS/soffice", - ] - } else { - vec![ - "/usr/bin/soffice", - "/usr/local/bin/soffice", - "/usr/lib/libreoffice/program/soffice", - "soffice", // Try PATH - ] - }; - - // Try each path - for path in paths { - if Path::new(path).exists() || which::which(path).is_ok() { - return Ok(path.to_string()); - } - } - - Err("LibreOffice not found. Please install LibreOffice or set LIBREOFFICE_PATH environment variable.".to_string()) -} - -fn convert_file(odt_path: &Path, dest_dir: &Path) -> Result<(), String> { - let soffice_path = get_soffice_path()?; - - // Verify input file exists and is readable - if !odt_path.exists() { - return Err(format!("Source file does not exist: {}", odt_path.display())); - } - - // Verify destination directory exists and is writable - if !dest_dir.exists() { - return Err(format!("Destination directory does not exist: {}", dest_dir.display())); - } - - // Try to get write permissions on destination directory - let metadata = fs::metadata(dest_dir) - .map_err(|e| format!("Failed to get destination directory metadata: {}", e))?; - - #[cfg(unix)] - use std::os::unix::fs::PermissionsExt; - #[cfg(unix)] - if metadata.permissions().mode() & 0o200 == 0 { - return Err(format!("Destination directory is not writable: {}", dest_dir.display())); - } - - let output = Command::new(&soffice_path) - .args(&[ - "--headless", - "--convert-to", - "pdf", - "--outdir", - dest_dir.to_str().ok_or("Invalid destination path")?, - odt_path.to_str().ok_or("Invalid source path")?, - ]) - .output() - .map_err(|e| format!("Failed to execute LibreOffice command: {}", e))?; - - if !output.status.success() { - let error_msg = String::from_utf8_lossy(&output.stderr); - let stdout_msg = String::from_utf8_lossy(&output.stdout); - return Err(format!( - "Conversion failed:\nError: {}\nOutput: {}", - error_msg, - stdout_msg - )); - } - - Ok(()) -} - -fn process_directory(source_dir: &Path, dest_dir: &Path) -> Result<(), String> { - // Create destination directory if it doesn't exist - if let Err(e) = fs::create_dir_all(dest_dir) { - return Err(format!("Error creating destination directory: {}", e)); - } - - let entries = fs::read_dir(source_dir) - .map_err(|e| format!("Error reading source directory: {}", e))?; - for entry in entries.flatten() { - let path = entry.path(); - - if path.is_dir() { - // Get the relative path from source to the current subdirectory - let relative_path = path.strip_prefix(source_dir) - .map_err(|e| format!("Error getting relative path: {}", e))?; - - // Create the corresponding destination subdirectory - let dest_subdir = dest_dir.join(relative_path); - - // Recursively process the subdirectory - process_directory(&path, &dest_subdir)?; - } else if path.extension().map_or(false, |ext| ext == "odt") { - // Get the relative path from source to the current file - let relative_path = path.strip_prefix(source_dir) - .map_err(|e| format!("Error getting relative path: {}", e))?; - - // Create the corresponding destination subdirectory if needed - if let Some(parent) = relative_path.parent() { - let dest_subdir = dest_dir.join(parent); - if let Err(e) = fs::create_dir_all(&dest_subdir) { - return Err(format!("Error creating destination subdirectory: {}", e)); - } - } - - // Construct the PDF path with the same directory structure - let pdf_path = dest_dir.join( - relative_path.with_extension("pdf") - ); - - if needs_conversion(&path, &pdf_path) { - match convert_file(&path, &pdf_path.parent().unwrap_or(dest_dir)) { - Ok(_) => println!("Converted: {} -> {}", relative_path.display(), pdf_path.display()), - Err(e) => eprintln!("Error: {} - {}", relative_path.display(), e), - } - } - } - } - Ok(()) -} - fn main() { let args = Args::parse(); - - if let Err(e) = process_directory(&args.source, &args.dest) { + + let mut processor = DirectoryProcessor::new(args.source, args.dest); + if let Err(e) = processor.process() { eprintln!("Error processing directory: {}", e); } } + diff --git a/src/tools.rs b/src/tools.rs new file mode 100644 index 0000000..58725c9 --- /dev/null +++ b/src/tools.rs @@ -0,0 +1,86 @@ +use std::path::Path; +use std::process::Command; +use std::env; + +pub fn get_soffice_path() -> Result { + // First try environment variable if set + if let Ok(path) = env::var("LIBREOFFICE_PATH") { + if Path::new(&path).exists() { + return Ok(path); + } + } + + // Check common installation paths + let paths = if cfg!(target_os = "macos") { + vec![ + "/Applications/LibreOffice.app/Contents/MacOS/soffice", + "/Applications/LibreOffice-still.app/Contents/MacOS/soffice", + "/Applications/LibreOffice-fresh.app/Contents/MacOS/soffice", + ] + } else { + vec![ + "/usr/bin/soffice", + "/usr/local/bin/soffice", + "/usr/lib/libreoffice/program/soffice", + "soffice", // Try PATH + ] + }; + + // Try each path + for path in paths { + if Path::new(path).exists() || which::which(path).is_ok() { + return Ok(path.to_string()); + } + } + + Err("LibreOffice not found. Please install LibreOffice or set LIBREOFFICE_PATH environment variable.".to_string()) +} + +pub fn convert_file(odt_path: &Path, dest_dir: &Path) -> Result<(), String> { + let soffice_path = get_soffice_path()?; + + // Verify input file exists and is readable + if !odt_path.exists() { + return Err(format!("Source file does not exist: {}", odt_path.display())); + } + + // Verify destination directory exists and is writable + if !dest_dir.exists() { + return Err(format!("Destination directory does not exist: {}", dest_dir.display())); + } + + // Try to get write permissions on destination directory + let metadata = std::fs::metadata(dest_dir) + .map_err(|e| format!("Failed to get destination directory metadata: {}", e))?; + + #[cfg(unix)] + use std::os::unix::fs::PermissionsExt; + #[cfg(unix)] + if metadata.permissions().mode() & 0o200 == 0 { + return Err(format!("Destination directory is not writable: {}", dest_dir.display())); + } + + let output = Command::new(&soffice_path) + .args(&[ + "--headless", + "--convert-to", + "pdf", + "--outdir", + dest_dir.to_str().ok_or("Invalid destination path")?, + odt_path.to_str().ok_or("Invalid source path")?, + ]) + .output() + .map_err(|e| format!("Failed to execute LibreOffice command: {}", e))?; + + if !output.status.success() { + let error_msg = String::from_utf8_lossy(&output.stderr); + let stdout_msg = String::from_utf8_lossy(&output.stdout); + return Err(format!( + "Conversion failed:\nError: {}\nOutput: {}", + error_msg, + stdout_msg + )); + } + + Ok(()) +} \ No newline at end of file