use crate::cli::{Config, ProcessMode, OverwritePolicy, Verbosity, ProcessStats};
use crate::error::{Error, Result};
use crate::utils;
use indicatif::{ProgressBar, ProgressStyle, MultiProgress};
use log::{debug, info, warn};
use rayon::prelude::*;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Debug)]
struct FileResult {
input: PathBuf,
output: PathBuf,
size: usize,
input_size: u64,
skipped: bool,
error: Option<Error>,
}
pub struct BatchProcessor {
config: Config,
}
impl BatchProcessor {
pub fn new(config: Config) -> Self {
Self { config }
}
pub fn process(&self) -> Result<ProcessStats> {
info!("Starting batch processing");
self.ensure_output_directory()?;
let total = self.config.input_files.len();
let mut stats = ProcessStats {
total,
quiet: self.config.verbosity == Verbosity::Quiet,
..Default::default()
};
stats.start();
if total == 1 || self.config.jobs == Some(1) {
debug!("Using sequential processing");
self.process_sequential(&mut stats)?;
} else {
debug!("Using parallel processing");
self.process_parallel(&mut stats)?;
}
stats.finish();
info!("Batch processing completed: {} success, {} failed, {} skipped",
stats.success, stats.failed, stats.skipped);
Ok(stats)
}
fn process_sequential(&self, stats: &mut ProcessStats) -> Result<()> {
let progress = self.create_progress_bar(stats.total)?;
for input_file in &self.config.input_files {
if let Some(ref pb) = progress {
pb.set_message(format!("处理: {}", input_file.display()));
}
let result = self.process_single_file(input_file);
self.handle_result(&result, stats);
if let Some(ref pb) = progress {
pb.inc(1);
}
}
if let Some(pb) = progress {
pb.finish_with_message("处理完成");
}
Ok(())
}
fn process_parallel(&self, stats: &mut ProcessStats) -> Result<()> {
let jobs = self.config.jobs.unwrap_or_else(num_cpus::get);
debug!("Using {} parallel workers", jobs);
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(jobs)
.build()
.map_err(|e| Error::Config {
message: format!("Failed to create thread pool: {}", e)
})?;
let multi_progress = if self.config.show_progress && self.config.verbosity != Verbosity::Quiet {
Some(Arc::new(MultiProgress::new()))
} else {
None
};
let progress = multi_progress.as_ref().map(|mp| {
let pb = mp.add(ProgressBar::new(stats.total as u64));
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})")
.unwrap()
.progress_chars("#>-")
);
pb
});
let results: Vec<FileResult> = pool.install(|| {
self.config.input_files
.par_iter()
.map(|input_file| {
let result = self.process_single_file(input_file);
if let Some(ref pb) = progress {
pb.inc(1);
}
result
})
.collect()
});
if let Some(pb) = progress {
pb.finish_with_message("处理完成");
}
for result in results {
self.handle_result(&result, stats);
}
Ok(())
}
fn process_single_file(&self, input_file: &Path) -> FileResult {
let output_file = self.determine_output_path(input_file);
let input_size = fs::metadata(input_file)
.map(|m| m.len())
.unwrap_or(0);
let final_output = match self.handle_overwrite_policy(&output_file) {
Ok(Some(path)) => path,
Ok(None) => {
return FileResult {
input: input_file.to_path_buf(),
output: output_file,
size: 0,
input_size,
skipped: true,
error: None,
};
}
Err(e) => {
return FileResult {
input: input_file.to_path_buf(),
output: output_file,
size: 0,
input_size,
skipped: false,
error: Some(e),
};
}
};
match self.extract_and_save(input_file, &final_output) {
Ok(size) => FileResult {
input: input_file.to_path_buf(),
output: final_output,
size,
input_size,
skipped: false,
error: None,
},
Err(e) => FileResult {
input: input_file.to_path_buf(),
output: final_output,
size: 0,
input_size,
skipped: false,
error: Some(e),
},
}
}
fn extract_and_save(&self, input: &Path, output: &Path) -> Result<usize> {
use rawlib::extract_thumbnail;
debug!("Processing file: {} -> {}", input.display(), output.display());
let thumb_bytes = extract_thumbnail(input)?;
fs::write(output, &thumb_bytes)?;
Ok(thumb_bytes.len())
}
fn handle_result(&self, result: &FileResult, stats: &mut ProcessStats) {
if result.skipped {
stats.skipped += 1;
if self.config.verbosity >= Verbosity::Normal {
println!("⊘ 跳过 (已存在): {}", result.output.display());
}
} else if let Some(ref e) = result.error {
stats.failed += 1;
stats.total_input_bytes += result.input_size;
warn!("Failed to process {}: {}", result.input.display(), e);
eprintln!("✗ {}: {}", result.input.display(), e);
} else {
stats.success += 1;
stats.total_input_bytes += result.input_size;
stats.total_output_bytes += result.size as u64;
if self.config.verbosity >= Verbosity::Normal {
println!("✓ {} -> {} ({} 字节)",
result.input.display(),
result.output.display(),
result.size);
}
if self.config.verbosity == Verbosity::Verbose {
self.print_thumbnail_info(&result.input);
}
}
}
fn ensure_output_directory(&self) -> Result<()> {
match self.config.mode {
ProcessMode::SingleFile => {
if let Some(parent) = self.config.output.parent() {
if !parent.exists() {
debug!("Creating parent directory: {}", parent.display());
fs::create_dir_all(parent)?;
if self.config.verbosity >= Verbosity::Verbose {
println!("创建目录: {}", parent.display());
}
}
}
}
ProcessMode::Batch => {
if !self.config.output.exists() {
debug!("Creating output directory: {}", self.config.output.display());
fs::create_dir_all(&self.config.output)?;
if self.config.verbosity >= Verbosity::Verbose {
println!("创建输出目录: {}", self.config.output.display());
}
}
}
}
Ok(())
}
fn create_progress_bar(&self, total: usize) -> Result<Option<ProgressBar>> {
if self.config.show_progress && self.config.verbosity != Verbosity::Quiet {
let pb = ProgressBar::new(total as u64);
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})\n{msg}")
.map_err(|e| Error::Config {
message: format!("Failed to set progress style: {}", e)
})?
.progress_chars("#>-")
);
Ok(Some(pb))
} else {
Ok(None)
}
}
fn determine_output_path(&self, input_file: &Path) -> PathBuf {
match self.config.mode {
ProcessMode::SingleFile => self.config.output.clone(),
ProcessMode::Batch => {
let file_stem = input_file.file_stem().unwrap();
let extension = self.config.format.extension();
self.config.output
.join(file_stem)
.with_extension(extension)
}
}
}
fn handle_overwrite_policy(&self, output_file: &Path) -> Result<Option<PathBuf>> {
if !output_file.exists() {
return Ok(Some(output_file.to_path_buf()));
}
match self.config.overwrite_policy {
OverwritePolicy::Skip => {
debug!("Skipping existing file: {}", output_file.display());
Ok(None)
}
OverwritePolicy::Overwrite => {
debug!("Overwriting existing file: {}", output_file.display());
Ok(Some(output_file.to_path_buf()))
}
OverwritePolicy::Rename => {
let renamed = utils::find_available_filename(output_file)?;
debug!("Renaming to: {}", renamed.display());
Ok(Some(renamed))
}
}
}
fn print_thumbnail_info(&self, input: &Path) {
use rawlib::extract_thumbnail_with_info;
if let Ok(thumb_info) = extract_thumbnail_with_info(input) {
println!(" 格式: {:?}, 尺寸: {}x{}, 颜色: {}, 位深: {}",
thumb_info.format,
thumb_info.width,
thumb_info.height,
thumb_info.colors,
thumb_info.bits);
}
}
}