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
This commit is contained in:
parent
68476f12a8
commit
4bfd43e1e0
177
src/directory_processor.rs
Normal file
177
src/directory_processor.rs
Normal file
@ -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<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<bool, String> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
169
src/main.rs
169
src/main.rs
@ -1,10 +1,12 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use std::fs;
|
use std::path::PathBuf;
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::process::Command;
|
mod tools;
|
||||||
use std::time::UNIX_EPOCH;
|
mod directory_processor;
|
||||||
use std::env;
|
|
||||||
// use std::time::{SystemTime, UNIX_EPOCH};
|
use directory_processor::DirectoryProcessor;
|
||||||
|
|
||||||
|
pub const FILES_TO_COPY: [&str; 10] = ["jpg", "jpeg", "png", "gif", "bmp", "tiff", "webp", "avif", "txt", "md"];
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(
|
#[command(
|
||||||
@ -20,159 +22,12 @@ struct Args {
|
|||||||
dest: PathBuf,
|
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<String, String> {
|
|
||||||
// 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() {
|
fn main() {
|
||||||
let args = Args::parse();
|
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);
|
eprintln!("Error processing directory: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
86
src/tools.rs
Normal file
86
src/tools.rs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
pub fn get_soffice_path() -> Result<String, String> {
|
||||||
|
// 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(())
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user