use crate::{Result, TreeEntry};
use rayon::prelude::*;
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
#[derive(Debug, Clone, Default)]
pub struct FileStats {
pub size: u64,
pub line_count: u64,
pub is_dir: bool,
}
#[derive(Debug, Clone, Default)]
pub struct TreeStats {
pub total_size: u64,
pub file_size: u64,
pub dir_size: u64,
pub file_count: usize,
pub dir_count: usize,
pub total_lines: u64,
}
impl TreeStats {
pub fn from_entries(entries: &[TreeEntry]) -> Self {
let mut stats = Self::default();
for entry in entries {
stats.add_entry(entry);
}
stats
}
fn add_entry(&mut self, entry: &TreeEntry) {
self.total_size += entry.size;
if entry.is_dir {
self.dir_count += 1;
self.dir_size += entry.size;
} else {
self.file_count += 1;
self.file_size += entry.size;
self.total_lines += entry.line_count;
}
for child in &entry.children {
self.add_entry(child);
}
}
}
pub fn count_lines(path: &Path, max_size: u64) -> Result<u64> {
let metadata = std::fs::metadata(path)?;
if metadata.len() > max_size {
log::debug!("Skipping line count for large file: {:?}", path);
return Ok(0);
}
if is_binary_file(path)? {
log::debug!("Skipping line count for binary file: {:?}", path);
return Ok(0);
}
let file = File::open(path)?;
let mut reader = BufReader::with_capacity(4096, file);
let mut count = 0u64;
let mut buffer = [0u8; 4096];
let mut last_byte = 0u8;
let mut has_content = false;
loop {
match reader.read(&mut buffer)? {
0 => break,
n => {
has_content = true;
count += buffer[..n].iter().filter(|&&b| b == b'\n').count() as u64;
last_byte = buffer[n - 1];
}
}
}
if has_content && last_byte != b'\n' {
count += 1;
}
Ok(count)
}
pub fn count_lines_parallel(paths: &[&Path], max_size: u64) -> Vec<u64> {
paths
.par_iter()
.map(|path| count_lines(path, max_size).unwrap_or(0))
.collect()
}
fn is_binary_file(path: &Path) -> Result<bool> {
let mut file = File::open(path)?;
let mut buffer = [0u8; 512];
let bytes_read = file.read(&mut buffer)?;
if bytes_read == 0 {
return Ok(false);
}
let null_count = buffer[..bytes_read].iter().filter(|&&b| b == 0).count();
if null_count > 0 {
return Ok(true);
}
let text_chars = buffer[..bytes_read]
.iter()
.filter(|&&b| b == b'\n' || b == b'\r' || b == b'\t' || (32..=126).contains(&b) || b >= 128)
.count();
Ok(text_chars < (bytes_read * 95) / 100)
}
pub fn calculate_dir_size(path: &Path) -> Result<u64> {
let size = Arc::new(AtomicU64::new(0));
calculate_dir_size_recursive(path, &size)?;
Ok(size.load(Ordering::Relaxed))
}
fn calculate_dir_size_recursive(path: &Path, size: &Arc<AtomicU64>) -> Result<()> {
let entries = std::fs::read_dir(path)?;
let mut dirs = Vec::new();
let mut files = Vec::new();
for entry in entries {
let entry = entry?;
let metadata = entry.metadata()?;
if metadata.is_dir() {
dirs.push(entry.path());
} else {
files.push(metadata.len());
}
}
let file_sum: u64 = files.iter().sum();
size.fetch_add(file_sum, Ordering::Relaxed);
dirs.par_iter()
.try_for_each(|dir| calculate_dir_size_recursive(dir, size))?;
Ok(())
}
pub fn format_duration(secs: u64) -> String {
if secs < 60 {
format!("{}s", secs)
} else if secs < 3600 {
format!("{}m {}s", secs / 60, secs % 60)
} else {
format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_count_lines() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "line 1").unwrap();
writeln!(file, "line 2").unwrap();
write!(file, "line 3").unwrap(); file.flush().unwrap();
let count = count_lines(file.path(), 1_000_000).unwrap();
assert_eq!(count, 3);
}
#[test]
fn test_is_binary_file() {
let mut text_file = NamedTempFile::new().unwrap();
writeln!(text_file, "This is a text file").unwrap();
assert!(!is_binary_file(text_file.path()).unwrap());
let mut bin_file = NamedTempFile::new().unwrap();
bin_file.write_all(&[0, 1, 2, 3, 255, 254]).unwrap();
assert!(is_binary_file(bin_file.path()).unwrap());
}
#[test]
fn test_format_duration() {
assert_eq!(format_duration(45), "45s");
assert_eq!(format_duration(125), "2m 5s");
assert_eq!(format_duration(3725), "1h 2m");
}
}