use super::error::TookaError;
use crate::{
common::logger::log_file_operation,
file::{file_match, file_ops},
rules::rules_file::RulesFile,
};
use rayon::prelude::*;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use walkdir::WalkDir;
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct MatchResult {
pub file_name: String,
pub action: String,
pub matched_rule_id: String,
pub current_path: PathBuf,
pub new_path: PathBuf,
}
pub fn sort_files<F>(
files: &[PathBuf],
source_path: &Path,
rules_file: &RulesFile,
dry_run: bool,
on_progress: Option<F>,
) -> Result<Vec<MatchResult>, TookaError>
where
F: Fn() + Send + Sync,
{
let progress = Arc::new(on_progress.map(|f| Arc::new(f)));
let results: Result<Vec<_>, TookaError> = files
.par_iter()
.map(|file_path| {
let res = sort_file(file_path, rules_file, dry_run, source_path);
if let Some(ref cb) = *progress {
cb();
}
res
})
.collect();
results.map(|v| v.into_iter().flatten().collect())
}
fn sort_file(
file_path: &Path,
rules_file: &RulesFile,
dry_run: bool,
source_path: &Path,
) -> Result<Vec<MatchResult>, TookaError> {
log::debug!("Processing file: '{}'", file_path.display());
let file_name = file_path
.file_name()
.and_then(|s| s.to_str())
.ok_or_else(|| {
TookaError::FileOperationError(format!(
"Failed to get file name from path '{}'",
file_path.display()
))
})?;
let Some(rule) = rules_file
.rules
.iter()
.find(|rule| file_match::match_rule_matcher(file_path, &rule.when))
else {
log::debug!("No matching rules found for file '{file_name}'");
return Ok(vec![MatchResult {
file_name: file_name.to_string(),
action: "skip".to_string(),
matched_rule_id: "none".to_string(),
current_path: file_path.to_path_buf(),
new_path: file_path.to_path_buf(),
}]);
};
log::debug!(
"File '{}' matched rule '{}' with priority {}",
file_name,
rule.id,
rule.priority
);
let mut results = Vec::with_capacity(rule.then.len());
let mut current_path = file_path.to_path_buf();
for (i, action) in rule.then.iter().enumerate() {
let op_result = file_ops::execute_action(¤t_path, action, dry_run, source_path)
.map_err(|e| {
TookaError::FileOperationError(format!("Failed to execute action: {e}"))
})?;
let log_prefix = if dry_run { "DRY" } else { "" };
log_file_operation(&format!(
"{log_prefix}[{action:?}] '{}' to '{}'",
current_path.display(),
op_result.new_path.display()
));
results.push(MatchResult {
file_name: file_name.to_string(),
action: op_result.action.clone(),
matched_rule_id: rule.id.clone(),
current_path: current_path.clone(),
new_path: op_result.new_path.clone(),
});
if op_result.action == "delete" {
if i + 1 < rule.then.len() {
log::warn!(
"File was deleted, skipping {} remaining action(s).",
rule.then.len() - (i + 1)
);
}
break;
}
current_path.clone_from(&op_result.new_path);
}
Ok(results)
}
pub fn collect_files(dir: &Path) -> Result<Vec<PathBuf>, TookaError> {
if !dir.exists() || !dir.is_dir() {
return Err(TookaError::ConfigError(format!(
"Path '{}' does not exist or is not a directory.",
dir.display()
)));
}
let files: Result<Vec<PathBuf>, std::io::Error> = WalkDir::new(dir)
.follow_links(false)
.into_iter()
.par_bridge()
.filter_map(|entry| match entry {
Ok(e) if e.file_type().is_file() => Some(Ok(e.path().to_path_buf())),
Ok(_) => None, Err(err) => {
log::warn!("Error reading directory entry: {err}");
None }
})
.collect();
files.map_err(|e| TookaError::FileOperationError(format!("Failed to collect files: {e}")))
}