use crate::types::{FileInfo, HashAlgorithm};
use std::fs;
use std::io::{self, Read, Write};
use std::path::Path;
use chrono::Utc;
pub fn can_delete(path: &Path) -> bool {
if let Some(parent) = path.parent() {
if let Ok(parent_meta) = fs::metadata(parent) {
!parent_meta.permissions().readonly()
} else {
false
}
} else {
false
}
}
pub fn get_file_extension(path: &Path) -> String {
let file_name = match path.file_name().and_then(|n| n.to_str()) {
Some(name) => name,
None => return "none".to_string(),
};
let parts: Vec<&str> = file_name.split('.').collect();
if parts.len() >= 2 {
format!(".{}", parts[1..].join("."))
} else {
"none".to_string()
}
}
pub fn get_file_size(path: &Path) -> u64 {
if path.is_file() {
fs::metadata(path).map(|m| m.len()).unwrap_or(0)
} else if path.is_dir() {
let mut total = 0;
if let Ok(entries) = fs::read_dir(path) {
for entry in entries.flatten() {
total += get_file_size(&entry.path());
}
}
total
} else {
0
}
}
pub fn get_file_age_seconds(path: &Path) -> i64 {
if let Ok(metadata) = fs::metadata(path) {
if let Ok(modified) = metadata.modified() {
if let Ok(duration) = std::time::SystemTime::now().duration_since(modified) {
return duration.as_secs() as i64;
}
}
}
0
}
#[allow(dead_code)]
pub fn is_empty_dir(path: &Path) -> bool {
if let Ok(entries) = fs::read_dir(path) {
entries.count() == 0
} else {
false
}
}
pub fn parse_size_threshold(s: &str) -> Result<u64, String> {
let s = s.trim();
fn try_parse_size(s: &str) -> Result<u64, String> {
let s_lower = s.to_lowercase();
let parts: Vec<&str> = s_lower.split_whitespace().collect();
let (num_str, unit) = match parts.as_slice() {
[num, unit] => (*num, *unit),
[combined] => {
if combined.ends_with("tib") {
(&combined[..combined.len() - 3], "tib")
} else if combined.ends_with("gib") {
(&combined[..combined.len() - 3], "gib")
} else if combined.ends_with("mib") {
(&combined[..combined.len() - 3], "mib")
} else if combined.ends_with("kib") {
(&combined[..combined.len() - 3], "kib")
} else if combined.ends_with("tb") {
(&combined[..combined.len() - 2], "tb")
} else if combined.ends_with("gb") {
(&combined[..combined.len() - 2], "gb")
} else if combined.ends_with("mb") {
(&combined[..combined.len() - 2], "mb")
} else if combined.ends_with("kb") {
(&combined[..combined.len() - 2], "kb")
} else if combined.ends_with("b") {
(&combined[..combined.len() - 1], "b")
} else if combined.chars().all(|c| c.is_ascii_digit() || c == '.') {
(*combined, "b")
} else {
return Err(format!("Invalid size format: {}", combined));
}
}
_ => return Err(format!("Invalid size format: {}", s)),
};
let num: f64 = num_str.parse().map_err(|_| format!("Invalid size number: {}", num_str))?;
match unit {
"b" => Ok(num.round() as u64),
"kb" => Ok((num * 1024.0).round() as u64),
"mb" => Ok((num * 1024.0 * 1024.0).round() as u64),
"gb" => Ok((num * 1024.0 * 1024.0 * 1024.0).round() as u64),
"tb" => Ok((num * 1024.0 * 1024.0 * 1024.0 * 1024.0).round() as u64),
"kib" => Ok((num * 1024.0).round() as u64),
"mib" => Ok((num * 1024.0 * 1024.0).round() as u64),
"gib" => Ok((num * 1024.0 * 1024.0 * 1024.0).round() as u64),
"tib" => Ok((num * 1024.0 * 1024.0 * 1024.0 * 1024.0).round() as u64),
_ => Err(format!("Unknown size unit: {}. Use b, kb, mb, gb, tb, kib, mib, gib, tib", unit)),
}
}
if let Ok(size) = try_parse_size(s) {
return Ok(size);
}
let path = Path::new(s);
if path.exists() && path.is_file() {
if let Ok(metadata) = fs::metadata(path) {
return Ok(metadata.len());
}
return Err(format!("Cannot read file metadata: {}", s));
}
Err(format!("Invalid size format or file not found: {}", s))
}
pub fn parse_age_threshold(s: &str) -> Result<i64, String> {
let s = s.trim().to_lowercase();
if let Ok(date) = chrono::NaiveDate::parse_from_str(&s, "%Y-%m-%d") {
if let Some(target_dt) = date.and_hms_opt(0, 0, 0) {
let target = chrono::DateTime::<Utc>::from_naive_utc_and_offset(target_dt, Utc).timestamp();
let now = chrono::Utc::now().timestamp();
return Ok(now - target);
}
}
let parts: Vec<&str> = s.split_whitespace().collect();
let (num_str, unit) = match parts.as_slice() {
[num, unit] => (*num, *unit),
[combined] => {
if combined.len() < 2 {
return Err(format!("Invalid age format: {}", combined));
}
(&combined[..combined.len() - 1], &combined[combined.len() - 1..])
}
_ => return Err(format!("Invalid age format: {}", s)),
};
let num: i64 = num_str.parse().map_err(|_| format!("Invalid number: {}", num_str))?;
match unit {
"d" => Ok(num * 86400),
"w" => Ok(num * 604800),
"m" => Ok(num * 2592000),
"y" => Ok(num * 31536000),
_ => Err(format!(
"Unknown time unit: '{}'. Use d, w, m, y or YYYY-MM-DD",
unit
)),
}
}
pub fn file_contains_text(path: &Path, pattern: &str) -> bool {
if let Ok(content) = fs::read_to_string(path) {
content.contains(pattern)
} else {
false
}
}
pub fn delete_duplicate_file(path: &Path, force: bool) -> bool {
if !force {
print!("Delete {}? (y/N): ", path.display());
io::stdout().flush().unwrap();
let mut input = String::new();
if io::stdin().read_line(&mut input).is_err() {
return false;
}
let input = input.trim().to_lowercase();
if input != "y" && input != "yes" {
return false;
}
}
fs::remove_file(path).is_ok()
}
pub fn merge_duplicate_file(path: &Path, target: &Path) -> bool {
if path == target {
return true;
}
let _ = fs::remove_file(path);
if fs::hard_link(target, path).is_ok() {
return true;
}
#[cfg(unix)]
{
if std::os::unix::fs::symlink(target, path).is_err() {
eprintln!(
"Error linking {} -> {}: hard link and symlink both failed",
path.display(),
target.display()
);
return false;
}
}
#[cfg(not(unix))]
{
if std::os::windows::fs::symlink_file(target, path).is_err() {
eprintln!(
"Error linking {} -> {}: hard link and symlink both failed",
path.display(),
target.display()
);
return false;
}
}
true
}
pub fn format_unix_permissions(metadata: &fs::Metadata, detailed: bool) -> String {
if detailed {
#[cfg(unix)]
let mode = {
use std::os::unix::fs::PermissionsExt;
metadata.permissions().mode()
};
#[cfg(windows)]
let mode = {
use std::os::windows::fs::MetadataExt;
let readonly = metadata.permissions().readonly();
let attrs = metadata.file_attributes();
let mut m: u32 = 0;
if !readonly {
m |= 0o222;
}
if attrs & 0x10 != 0 {
m |= 0o111;
}
if attrs & 0x20 != 0 || attrs & 0x40 != 0 {
m |= 0o444;
}
m
};
let file_type = if metadata.is_dir() { 'd' } else { '-' };
let user_read = if mode & 0o400 != 0 { 'r' } else { '-' };
let user_write = if mode & 0o200 != 0 { 'w' } else { '-' };
let user_exec = if mode & 0o100 != 0 { 'x' } else { '-' };
let group_read = if mode & 0o040 != 0 { 'r' } else { '-' };
let group_write = if mode & 0o020 != 0 { 'w' } else { '-' };
let group_exec = if mode & 0o010 != 0 { 'x' } else { '-' };
let other_read = if mode & 0o004 != 0 { 'r' } else { '-' };
let other_write = if mode & 0o002 != 0 { 'w' } else { '-' };
let other_exec = if mode & 0o001 != 0 { 'x' } else { '-' };
format!(
"{}{}{}{}{}{}{}{}{}{}",
file_type, user_read, user_write, user_exec,
group_read, group_write, group_exec,
other_read, other_write, other_exec
)
} else {
#[cfg(unix)]
{
if metadata.permissions().readonly() {
if can_delete(&std::path::Path::new("")) {
"r-x"
} else {
"r--"
}
} else {
if can_delete(&std::path::Path::new("")) {
"rwx"
} else {
"rw-"
}
}
.to_string()
}
#[cfg(windows)]
{
use std::os::windows::fs::MetadataExt;
let readonly = metadata.permissions().readonly();
let attrs = metadata.file_attributes();
let is_dir = attrs & 0x10 != 0;
let is_hidden = attrs & 0x2 != 0;
let mut perms = String::new();
if is_dir {
perms.push('d');
} else {
perms.push('-');
}
if is_hidden {
perms.push_str("h");
}
if readonly {
perms.push_str("r--");
} else {
perms.push_str("rw-");
}
if is_dir {
perms.push('x');
} else {
perms.push('-');
}
perms
}
}
}
pub fn filter_files(files: Vec<FileInfo>, exclude_dirs: bool) -> Vec<FileInfo> {
if exclude_dirs {
files.into_iter().filter(|f| !f.is_directory).collect()
} else {
files
}
}
pub fn preview_file(path: &Path, lines: usize, mode: &str) {
match fs::read_to_string(path) {
Ok(content) => {
let file_lines: Vec<&str> = content.lines().collect();
let total = file_lines.len();
if total == 0 {
println!("(empty file)");
return;
}
println!("");
if mode == "first" {
println!("Preview (first {} lines):", lines);
} else if mode == "last" {
println!("Preview (last {} lines):", lines);
} else {
println!("Preview (first {} / last {} lines):", lines, lines);
}
println!("{}", "─".repeat(50));
if total <= lines * 2 && mode == "both" {
for line in &file_lines {
println!("{}", line);
}
} else if mode == "first" {
let head_end = lines.min(total);
for line in file_lines[..head_end].iter() {
println!("{}", line);
}
} else if mode == "last" {
let tail_start = total.saturating_sub(lines);
for line in file_lines[tail_start..].iter() {
println!("{}", line);
}
} else {
let head_end = lines.min(total);
for line in file_lines[..head_end].iter() {
println!("{}", line);
}
println!("{}", "... (lines omitted) ...");
let tail_start = total.saturating_sub(lines);
for line in file_lines[tail_start..].iter() {
println!("{}", line);
}
}
}
Err(_) => {
eprintln!("Error: Could not read file (not a text file or permission denied)");
}
}
}
pub fn compute_file_hash(path: &Path, algorithm: HashAlgorithm) -> Option<String> {
let file = match fs::File::open(path) {
Ok(f) => f,
Err(_) => return None,
};
let mut reader = std::io::BufReader::new(file);
let mut buffer = [0u8; 65536];
match algorithm {
HashAlgorithm::Sha256 => {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
loop {
let bytes_read = match reader.read(&mut buffer) {
Ok(0) => break,
Ok(n) => n,
Err(_) => return None,
};
hasher.update(&buffer[..bytes_read]);
}
let result = hasher.finalize();
Some(format!("{:x}", result))
}
HashAlgorithm::Md5 => {
use md5::{Digest, Md5};
let mut hasher = Md5::new();
loop {
let bytes_read = match reader.read(&mut buffer) {
Ok(0) => break,
Ok(n) => n,
Err(_) => return None,
};
hasher.update(&buffer[..bytes_read]);
}
let result = hasher.finalize();
Some(format!("{:x}", result))
}
}
}