use super::args::IngestArgs;
use super::scan_fs::{derive_kebab_name, unique_name, validate_name_prefix};
use crate::constants::DERIVED_NAME_MAX_LEN;
use crate::errors::AppError;
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
pub(super) enum SlotMeta {
Skip {
file_str: String,
derived_base: String,
name_truncated: bool,
original_name: Option<String>,
original_filename: Option<String>,
reason: String,
},
Process {
file_str: String,
derived_name: String,
name_truncated: bool,
original_name: Option<String>,
original_filename: Option<String>,
},
}
pub(super) struct ProcessItem {
pub(super) idx: usize,
pub(super) path: PathBuf,
pub(super) file_str: String,
pub(super) derived_name: String,
}
pub(super) struct IngestPlan {
pub(super) slots_meta: Vec<SlotMeta>,
pub(super) process_items: Vec<ProcessItem>,
}
fn reserve<T>(capacity: usize, what: &str) -> Result<Vec<T>, AppError> {
let mut v: Vec<T> = Vec::new();
v.try_reserve(capacity).map_err(|_| {
AppError::LimitExceeded(crate::i18n::errors_ops::allocation_would_exceed_memory(
capacity, what,
))
})?;
Ok(v)
}
pub(super) fn build_plan(args: &IngestArgs, files: &[PathBuf]) -> Result<IngestPlan, AppError> {
let files_cap = files.len();
let mut slots_meta: Vec<SlotMeta> = reserve(
files_cap,
crate::i18n::errors_ops::alloc_label_slot_metadata(),
)?;
let mut process_items: Vec<ProcessItem> = reserve(
files_cap,
crate::i18n::errors_ops::alloc_label_process_items(),
)?;
let mut truncations: Vec<(String, String)> = reserve(
files_cap,
crate::i18n::errors_ops::alloc_label_truncation_entries(),
)?;
let max_name_length = match args.name_prefix.as_deref() {
Some(prefix) => validate_name_prefix(prefix, args.max_name_length)?,
None => args.max_name_length,
};
let mut taken_names: BTreeSet<String> = BTreeSet::new();
for path in files {
plan_one(
path,
args,
max_name_length,
&mut taken_names,
&mut slots_meta,
&mut process_items,
&mut truncations,
);
}
if !truncations.is_empty() {
tracing::info!(
target: "ingest",
count = truncations.len(),
max_name_length = max_name_length,
max_len = DERIVED_NAME_MAX_LEN,
"derived names truncated; pass -vv (debug) for per-file detail"
);
}
Ok(IngestPlan {
slots_meta,
process_items,
})
}
fn plan_one(
path: &Path,
args: &IngestArgs,
max_name_length: usize,
taken_names: &mut BTreeSet<String>,
slots_meta: &mut Vec<SlotMeta>,
process_items: &mut Vec<ProcessItem>,
truncations: &mut Vec<(String, String)>,
) {
let file_str = path.to_string_lossy().into_owned();
let (derived_base, name_truncated, original_name) = derive_kebab_name(path, max_name_length);
let original_basename = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
if name_truncated {
if let Some(ref orig) = original_name {
truncations.push((orig.clone(), derived_base.clone()));
}
}
if derived_base.is_empty() {
let orig_filename = if original_basename.is_empty() {
None
} else {
Some(original_basename.to_string())
};
slots_meta.push(SlotMeta::Skip {
file_str,
derived_base: String::new(),
name_truncated: false,
original_name: None,
original_filename: orig_filename,
reason: "could not derive a non-empty kebab-case name from filename".to_string(),
});
return;
}
let derived_base = match args.name_prefix.as_deref() {
Some(prefix) => format!("{prefix}{derived_base}"),
None => derived_base,
};
match unique_name(&derived_base, taken_names) {
Ok(derived_name) => {
taken_names.insert(derived_name.clone());
let idx = slots_meta.len();
let orig_filename = if original_basename == derived_name {
None
} else {
Some(original_basename.to_string())
};
process_items.push(ProcessItem {
idx,
path: path.to_path_buf(),
file_str: file_str.clone(),
derived_name: derived_name.clone(),
});
slots_meta.push(SlotMeta::Process {
file_str,
derived_name,
name_truncated,
original_name,
original_filename: orig_filename,
});
}
Err(e) => {
let orig_filename = if original_basename == derived_base {
None
} else {
Some(original_basename.to_string())
};
slots_meta.push(SlotMeta::Skip {
file_str,
derived_base,
name_truncated,
original_name,
original_filename: orig_filename,
reason: e.to_string(),
});
}
}
}