use std::collections::HashMap;
use std::error::Error;
use std::path::{Path, PathBuf};
use glob::Pattern;
use rayon::prelude::*;
use walkdir::WalkDir;
pub fn collect_nc_files(src_dir: &Path, pattern: &str) -> Result<Vec<PathBuf>, Box<dyn Error>> {
let glob_pat = Pattern::new(pattern)
.map_err(|e| format!("Invalid pattern '{}': {}", pattern, e))?;
let files: Vec<PathBuf> = WalkDir::new(src_dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
.filter(|e| {
e.path()
.file_name()
.and_then(|n| n.to_str())
.map_or(false, |name| glob_pat.matches(name))
})
.map(|e| e.path().to_path_buf())
.collect();
Ok(files)
}
pub fn compute_output_pairs(
nc_files: &[PathBuf],
output_dir: Option<&Path>,
output_ext: &str,
) -> Result<Vec<(PathBuf, PathBuf)>, Box<dyn Error>> {
nc_files
.iter()
.map(|src| {
let stem = src
.file_stem()
.ok_or_else(|| format!("Cannot get file stem for {}", src.display()))?;
let dest = match output_dir {
Some(dir) => dir.join(stem).with_extension(output_ext),
None => src.with_extension(output_ext),
};
Ok((src.clone(), dest))
})
.collect()
}
pub fn check_duplicates(pairs: &[(PathBuf, PathBuf)]) -> Result<(), Box<dyn Error>> {
let mut seen: HashMap<&PathBuf, &PathBuf> = HashMap::new();
let mut duplicates: Vec<String> = Vec::new();
for (src, dest) in pairs {
if let Some(existing_src) = seen.insert(dest, src) {
duplicates.push(format!(
" {} and {} both map to {}",
existing_src.display(),
src.display(),
dest.display()
));
}
}
if !duplicates.is_empty() {
return Err(
format!("Duplicate output filenames detected:\n{}", duplicates.join("\n")).into(),
);
}
Ok(())
}
pub fn run_batch<F>(
src_dir: &Path,
output_dir: Option<&Path>,
threads: Option<usize>,
output_ext: &str,
pattern: &str,
process_fn: F,
) -> Result<usize, Box<dyn Error>>
where
F: Fn(&str, &str) -> Result<(), Box<dyn Error>> + Sync + Send,
{
let nc_files = collect_nc_files(src_dir, pattern)?;
if nc_files.is_empty() {
eprintln!(
"No files matching '{}' under {}; nothing to do.",
pattern,
src_dir.display()
);
return Ok(0);
}
let mut pairs = compute_output_pairs(&nc_files, output_dir, output_ext)?;
check_duplicates(&pairs)?;
pairs.sort_by_key(|(src, _)| {
std::cmp::Reverse(std::fs::metadata(src).map(|m| m.len()).unwrap_or(0))
});
if let Some(dir) = output_dir {
std::fs::create_dir_all(dir)
.map_err(|e| format!("Cannot create output directory {}: {}", dir.display(), e))?;
}
if std::env::var_os("POLARS_MAX_THREADS").is_none() {
std::env::set_var("POLARS_MAX_THREADS", "1");
}
let run = || -> Vec<String> {
pairs
.par_iter()
.with_max_len(1)
.filter_map(|(src, dest)| {
process_fn(
src.to_str().unwrap_or_default(),
dest.to_str().unwrap_or_default(),
)
.err()
.map(|e| format!("{}: {}", src.display(), e))
})
.collect()
};
const WORKER_STACK_SIZE: usize = 16 * 1024 * 1024;
let mut builder = rayon::ThreadPoolBuilder::new().stack_size(WORKER_STACK_SIZE);
if let Some(n) = threads {
builder = builder.num_threads(n);
}
let errors = builder
.build()
.map_err(|e| format!("Failed to build thread pool: {}", e))?
.install(run);
if !errors.is_empty() {
return Err(format!("Batch processing errors:\n{}", errors.join("\n")).into());
}
Ok(pairs.len())
}
#[cfg(test)]
mod tests {
use super::{check_duplicates, compute_output_pairs};
use std::path::PathBuf;
#[test]
fn test_output_pairs_flat_into_dir() {
let files = vec![
PathBuf::from("/data/a/file1.nc"),
PathBuf::from("/data/b/file2.nc"),
];
let out = PathBuf::from("/out");
let pairs = compute_output_pairs(&files, Some(&out), "parquet").unwrap();
assert_eq!(pairs[0], (PathBuf::from("/data/a/file1.nc"), PathBuf::from("/out/file1.parquet")));
assert_eq!(pairs[1], (PathBuf::from("/data/b/file2.nc"), PathBuf::from("/out/file2.parquet")));
}
#[test]
fn test_output_pairs_in_place() {
let files = vec![PathBuf::from("/data/sub/file.nc")];
let pairs = compute_output_pairs(&files, None, "parquet").unwrap();
assert_eq!(pairs[0].1, PathBuf::from("/data/sub/file.parquet"));
}
#[test]
fn test_output_pairs_yaml_extension() {
let files = vec![PathBuf::from("/data/file.nc")];
let pairs = compute_output_pairs(&files, None, "yaml").unwrap();
assert_eq!(pairs[0].1, PathBuf::from("/data/file.yaml"));
}
#[test]
fn test_output_pairs_preserves_stem_with_hyphens() {
let files = vec![PathBuf::from("/data/AR_PR_CT_ITP-71.nc")];
let pairs = compute_output_pairs(&files, None, "parquet").unwrap();
assert_eq!(pairs[0].1, PathBuf::from("/data/AR_PR_CT_ITP-71.parquet"));
}
#[test]
fn test_output_pairs_empty_input() {
let pairs = compute_output_pairs(&[], None, "parquet").unwrap();
assert!(pairs.is_empty());
}
#[test]
fn test_output_pairs_src_preserved() {
let f = PathBuf::from("/data/file.nc");
let pairs = compute_output_pairs(&[f.clone()], None, "parquet").unwrap();
assert_eq!(pairs[0].0, f);
}
#[test]
fn test_check_duplicates_no_conflict() {
let pairs = vec![
(PathBuf::from("/a/f1.nc"), PathBuf::from("/out/f1.parquet")),
(PathBuf::from("/b/f2.nc"), PathBuf::from("/out/f2.parquet")),
];
assert!(check_duplicates(&pairs).is_ok());
}
#[test]
fn test_check_duplicates_detects_conflict() {
let pairs = vec![
(PathBuf::from("/a/file.nc"), PathBuf::from("/out/file.parquet")),
(PathBuf::from("/b/file.nc"), PathBuf::from("/out/file.parquet")),
];
let err = check_duplicates(&pairs).unwrap_err().to_string();
assert!(err.contains("Duplicate output filenames detected"), "got: {err}");
}
#[test]
fn test_check_duplicates_error_names_conflicting_sources() {
let pairs = vec![
(PathBuf::from("/a/file.nc"), PathBuf::from("/out/file.parquet")),
(PathBuf::from("/b/file.nc"), PathBuf::from("/out/file.parquet")),
];
let err = check_duplicates(&pairs).unwrap_err().to_string();
assert!(err.contains("file.nc"), "got: {err}");
}
#[test]
fn test_check_duplicates_empty() {
assert!(check_duplicates(&[]).is_ok());
}
#[test]
fn test_check_duplicates_single_file() {
let pairs = vec![(PathBuf::from("/a/f.nc"), PathBuf::from("/out/f.parquet"))];
assert!(check_duplicates(&pairs).is_ok());
}
}