use std::path::{Path, PathBuf};
use crate::history::RenameAction;
use thiserror::Error;
use tokio::fs;
#[derive(Error, Debug)]
pub enum FrenError {
#[error("File system error: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid glob pattern: {0}")]
Pattern(String),
#[error("Regex error: {0}")]
Regex(#[from] regex::Error),
#[error("Pattern application error: {0}")]
PatternApplication(String),
}
#[derive(Debug, Clone)]
pub struct FileRename {
pub old_path: PathBuf,
pub new_path: PathBuf,
pub new_name: String,
}
#[derive(Debug)]
pub struct RenameExecutionResult {
pub successful: Vec<RenameAction>,
pub skipped: Vec<(PathBuf, String)>,
pub errors: Vec<(PathBuf, String)>,
}
pub async fn find_matching_files(pattern: &str) -> Result<Vec<PathBuf>, FrenError> {
find_matching_files_recursive(pattern, false).await
}
pub async fn find_matching_files_recursive(pattern: &str, recursive: bool) -> Result<Vec<PathBuf>, FrenError> {
use regex::Regex;
let has_glob_chars = pattern.contains('*') || pattern.contains('?') || pattern.contains('[') || pattern.contains(']');
let pattern = if !has_glob_chars {
let path = PathBuf::from(pattern);
let metadata = fs::metadata(&path).await;
if let Ok(meta) = metadata {
if meta.is_file() {
return Ok(vec![path]);
} else if meta.is_dir() && recursive {
format!("{}/**/*", pattern)
} else if meta.is_dir() {
return Ok(Vec::new());
} else {
pattern.to_string()
}
} else {
pattern.to_string()
}
} else {
pattern.to_string()
};
let is_hidden_pattern = pattern.starts_with('.') || pattern.contains("/.");
let (base_dir, normalized_pattern) = if pattern.contains("**") {
if let Some(star_pos) = pattern.find("**") {
if star_pos == 0 {
(PathBuf::from("."), pattern.to_string())
} else {
let before_star = &pattern[..star_pos];
let before_star = before_star.trim_end_matches('/');
if before_star.is_empty() {
(PathBuf::from("."), pattern.to_string())
} else {
let base_path = PathBuf::from(before_star);
if base_path.is_absolute() {
let pattern_after_base = &pattern[star_pos..];
(base_path, pattern_after_base.to_string())
} else {
(PathBuf::from("."), pattern.to_string())
}
}
}
} else {
(PathBuf::from("."), pattern.to_string())
}
} else if let Some(slash_pos) = pattern.rfind('/') {
let base = &pattern[..slash_pos];
let file_part = &pattern[slash_pos + 1..];
if pattern.starts_with('/') {
let base_path = PathBuf::from(base);
if base_path.is_absolute() {
(base_path, file_part.to_string())
} else {
(PathBuf::from(if base.is_empty() { "." } else { base }), file_part.to_string())
}
} else {
(PathBuf::from(if base.is_empty() { "." } else { base }), file_part.to_string())
}
} else {
(PathBuf::from("."), pattern.to_string())
};
let regex_pattern = glob_to_regex(&normalized_pattern, recursive)?;
let re = Regex::new(®ex_pattern)
.map_err(|e| FrenError::Pattern(format!("Invalid pattern: {}", e)))?;
let files = if recursive || pattern.contains("**") {
find_files_recursive_async(&base_dir, &re, is_hidden_pattern).await?
} else {
find_files_in_dir_async(&base_dir, &re, is_hidden_pattern).await?
};
Ok(files)
}
fn glob_to_regex(pattern: &str, recursive: bool) -> Result<String, FrenError> {
let mut regex = String::new();
regex.push('^');
let mut pattern = pattern.to_string();
if recursive && !pattern.contains("**") {
if pattern.starts_with("./") {
pattern = format!("./**/{}", &pattern[2..]);
} else if pattern.starts_with('/') {
pattern = format!("/**{}", pattern);
} else {
pattern = format!("**/{}", pattern);
}
}
let chars: Vec<char> = pattern.chars().collect();
let mut i = 0;
while i < chars.len() {
match chars[i] {
'*' => {
if i + 1 < chars.len() && chars[i + 1] == '*' {
if i + 2 < chars.len() && chars[i + 2] == '/' {
regex.push_str(".*"); i += 3; } else {
regex.push_str(".*"); i += 2;
}
} else {
regex.push_str("[^/]*");
i += 1;
}
}
'?' => {
regex.push_str("[^/]");
i += 1;
}
'.' => {
regex.push_str("\\.");
i += 1;
}
'/' => {
regex.push('/');
i += 1;
}
'[' => {
regex.push('[');
i += 1;
while i < chars.len() && chars[i] != ']' {
regex.push(chars[i]);
i += 1;
}
if i < chars.len() {
regex.push(']');
i += 1;
}
}
c => {
if c.is_alphanumeric() || c == '_' || c == '-' {
regex.push(c);
} else {
regex.push('\\');
regex.push(c);
}
i += 1;
}
}
}
regex.push('$');
Ok(regex)
}
async fn find_files_recursive_async(
dir: &Path,
pattern: ®ex::Regex,
include_hidden: bool,
) -> Result<Vec<PathBuf>, FrenError> {
let mut files = Vec::new();
let base_dir = fs::canonicalize(dir).await.unwrap_or_else(|_| dir.to_path_buf());
let mut dirs_to_search = vec![base_dir.clone()];
while let Some(current_dir) = dirs_to_search.pop() {
let mut entries = fs::read_dir(¤t_dir).await?;
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
let metadata = fs::metadata(&path).await?;
if metadata.is_file() {
let path_str = if let Ok(rel_path) = path.strip_prefix(&base_dir) {
rel_path.to_string_lossy().replace('\\', "/")
} else {
if let Ok(cwd) = std::env::current_dir() {
if let Ok(cwd_canon) = fs::canonicalize(&cwd).await {
if let Ok(rel_path) = path.strip_prefix(&cwd_canon) {
rel_path.to_string_lossy().replace('\\', "/")
} else {
path.to_string_lossy().replace('\\', "/")
}
} else {
path.to_string_lossy().replace('\\', "/")
}
} else {
path.to_string_lossy().replace('\\', "/")
}
};
if pattern.is_match(&path_str) {
let is_hidden = path.file_name()
.and_then(|n| n.to_str())
.map(|s| s.starts_with('.'))
.unwrap_or(false);
if include_hidden || !is_hidden {
files.push(path);
}
}
} else if metadata.is_dir() {
let is_hidden = path.file_name()
.and_then(|n| n.to_str())
.map(|s| s.starts_with('.'))
.unwrap_or(false);
if include_hidden || !is_hidden {
if let Ok(canon_path) = fs::canonicalize(&path).await {
dirs_to_search.push(canon_path);
} else {
dirs_to_search.push(path);
}
}
}
}
}
Ok(files)
}
async fn find_files_in_dir_async(
dir: &Path,
pattern: ®ex::Regex,
include_hidden: bool,
) -> Result<Vec<PathBuf>, FrenError> {
let mut files = Vec::new();
let base_dir = fs::canonicalize(dir).await.unwrap_or_else(|_| dir.to_path_buf());
let mut entries = fs::read_dir(&base_dir).await?;
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
let metadata = fs::metadata(&path).await?;
if metadata.is_file() {
let path_str = if let Ok(rel_path) = path.strip_prefix(&base_dir) {
rel_path.to_string_lossy().replace('\\', "/")
} else {
path.to_string_lossy().replace('\\', "/")
};
if pattern.is_match(&path_str) {
let is_hidden = path.file_name()
.and_then(|n| n.to_str())
.map(|s| s.starts_with('.'))
.unwrap_or(false);
if include_hidden || !is_hidden {
files.push(path);
}
}
}
}
Ok(files)
}
pub async fn perform_renames(
renames: &[FileRename],
overwrite: bool,
) -> Result<RenameExecutionResult, FrenError> {
let mut result = RenameExecutionResult {
successful: Vec::new(),
skipped: Vec::new(),
errors: Vec::new(),
};
let rename_futures = renames.iter().map(|rename| async move {
if rename.old_path == rename.new_path {
return (rename.old_path.clone(), None, Some("New name is identical to old name".to_string()));
}
if fs::metadata(&rename.new_path).await.is_ok() && !overwrite {
return (rename.old_path.clone(), None, Some("Target file already exists".to_string()));
}
match fs::rename(&rename.old_path, &rename.new_path).await {
Ok(_) => {
let action = RenameAction {
old_path: rename.old_path.clone(),
new_path: rename.new_path.clone(),
};
(rename.old_path.clone(), Some(action), None)
}
Err(e) => {
(rename.old_path.clone(), None, Some(e.to_string()))
}
}
});
let results: Vec<_> = futures::future::join_all(rename_futures).await;
for (path, action, issue) in results {
match (action, issue) {
(Some(action), None) => result.successful.push(action),
(None, Some(reason)) if reason.contains("identical") || reason.contains("already exists") => {
result.skipped.push((path, reason));
}
(None, Some(error)) => {
result.errors.push((path, error));
}
_ => {}
}
}
Ok(result)
}