use anyhow::{Context, Result};
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
pub fn read_file_from_end<P: AsRef<Path>>(path: P, _buffer_size: usize) -> Result<Vec<String>> {
let file = File::open(&path)
.with_context(|| format!("Failed to open file: {}", path.as_ref().display()))?;
let reader = BufReader::new(file);
let mut lines = Vec::new();
for line_result in reader.lines() {
let line = line_result?;
if !line.trim().is_empty() {
lines.push(line.trim().to_string());
}
}
Ok(lines)
}
pub fn is_file_readable<P: AsRef<Path>>(path: P) -> bool {
File::open(path).is_ok()
}
pub fn get_file_size<P: AsRef<Path>>(path: P) -> Result<u64> {
let metadata = std::fs::metadata(path).with_context(|| "Failed to get file metadata")?;
Ok(metadata.len())
}
pub fn is_file_rotated<P: AsRef<Path>>(path: P, previous_size: u64) -> Result<bool> {
let current_size = get_file_size(path)?;
Ok(current_size < previous_size)
}
pub fn validate_files<P: AsRef<Path> + Clone>(files: &[P]) -> Result<Vec<P>> {
let mut valid_files = Vec::new();
let mut errors = Vec::new();
for file in files {
if is_file_readable(file) {
valid_files.push(file.clone());
} else {
errors.push(format!("File not readable: {}", file.as_ref().display()));
}
}
if valid_files.is_empty() {
return Err(anyhow::anyhow!(
"No valid files to watch: {}",
errors.join(", ")
));
}
if !errors.is_empty() {
eprintln!(
"Warning: Some files are not accessible: {}",
errors.join(", ")
);
}
Ok(valid_files)
}
pub fn format_file_size(size: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
let mut size = size as f64;
let mut unit_index = 0;
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
size /= 1024.0;
unit_index += 1;
}
if unit_index == 0 {
format!("{} {}", size as u64, UNITS[unit_index])
} else {
format!("{:.1} {}", size, UNITS[unit_index])
}
}
pub fn get_filename<P: AsRef<Path>>(path: P) -> String {
path.as_ref()
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("unknown")
.to_string()
}
pub fn is_symlink<P: AsRef<Path>>(path: P) -> bool {
path.as_ref()
.symlink_metadata()
.map(|metadata| metadata.file_type().is_symlink())
.unwrap_or(false)
}
pub fn resolve_symlink<P: AsRef<Path>>(path: P) -> Result<std::path::PathBuf> {
let resolved = path
.as_ref()
.read_link()
.with_context(|| format!("Failed to read symlink: {}", path.as_ref().display()))?;
Ok(resolved)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_format_file_size() {
assert_eq!(format_file_size(1024), "1.0 KB");
assert_eq!(format_file_size(1536), "1.5 KB");
assert_eq!(format_file_size(1048576), "1.0 MB");
assert_eq!(format_file_size(1023), "1023 B");
}
#[test]
fn test_get_filename() {
assert_eq!(get_filename("/path/to/file.log"), "file.log");
assert_eq!(get_filename("file.log"), "file.log");
assert_eq!(get_filename("/"), "unknown");
}
#[test]
fn test_validate_files() {
let temp_file = NamedTempFile::new().unwrap();
let temp_path = temp_file.path();
let valid_files = vec![temp_path];
let result = validate_files(&valid_files);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 1);
let invalid_files = vec![std::path::Path::new("/nonexistent/file.log")];
let result = validate_files(&invalid_files);
assert!(result.is_err());
}
#[test]
fn test_read_file_from_end() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "line 1").unwrap();
writeln!(temp_file, "line 2").unwrap();
writeln!(temp_file, "line 3").unwrap();
temp_file.flush().unwrap();
let lines = read_file_from_end(temp_file.path(), 1024).unwrap();
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], "line 1");
assert_eq!(lines[1], "line 2");
assert_eq!(lines[2], "line 3");
}
#[test]
fn test_read_file_from_end_with_empty_lines() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "line 1").unwrap();
writeln!(temp_file).unwrap(); writeln!(temp_file, " ").unwrap(); writeln!(temp_file, "line 2").unwrap();
temp_file.flush().unwrap();
let lines = read_file_from_end(temp_file.path(), 1024).unwrap();
assert_eq!(lines.len(), 2); assert_eq!(lines[0], "line 1");
assert_eq!(lines[1], "line 2");
}
#[test]
fn test_read_file_from_end_with_nonexistent_file() {
let result = read_file_from_end("/nonexistent/file.log", 1024);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Failed to open file"));
}
#[test]
fn test_is_file_readable() {
let temp_file = NamedTempFile::new().unwrap();
assert!(is_file_readable(temp_file.path()));
assert!(!is_file_readable("/nonexistent/file.log"));
}
#[test]
fn test_get_file_size() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "test content").unwrap();
temp_file.flush().unwrap();
let size = get_file_size(temp_file.path()).unwrap();
assert!(size > 0);
}
#[test]
fn test_get_file_size_nonexistent() {
let result = get_file_size("/nonexistent/file.log");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Failed to get file metadata"));
}
#[test]
fn test_is_file_rotated() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "initial content").unwrap();
temp_file.flush().unwrap();
let initial_size = get_file_size(temp_file.path()).unwrap();
writeln!(temp_file, "more content").unwrap();
temp_file.flush().unwrap();
assert!(!is_file_rotated(temp_file.path(), initial_size).unwrap());
temp_file.as_file_mut().set_len(0).unwrap();
temp_file.flush().unwrap();
assert!(is_file_rotated(temp_file.path(), initial_size).unwrap());
}
#[test]
fn test_validate_files_with_mixed_validity() {
let temp_file = NamedTempFile::new().unwrap();
let temp_path = temp_file.path();
let files = vec![
temp_path,
std::path::Path::new("/nonexistent/file1.log"),
std::path::Path::new("/nonexistent/file2.log"),
];
let result = validate_files(&files);
assert!(result.is_ok());
let valid_files = result.unwrap();
assert_eq!(valid_files.len(), 1);
assert_eq!(valid_files[0], temp_path);
}
#[test]
fn test_validate_files_all_invalid() {
let files = vec![
std::path::Path::new("/nonexistent/file1.log"),
std::path::Path::new("/nonexistent/file2.log"),
];
let result = validate_files(&files);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("No valid files to watch"));
}
#[test]
fn test_format_file_size_edge_cases() {
assert_eq!(format_file_size(0), "0 B");
assert_eq!(format_file_size(1), "1 B");
assert_eq!(format_file_size(1023), "1023 B");
assert_eq!(format_file_size(1024), "1.0 KB");
assert_eq!(format_file_size(1536), "1.5 KB");
assert_eq!(format_file_size(2048), "2.0 KB");
assert_eq!(format_file_size(1048576), "1.0 MB");
assert_eq!(format_file_size(1572864), "1.5 MB");
assert_eq!(format_file_size(1073741824), "1.0 GB");
assert_eq!(format_file_size(1099511627776), "1.0 TB");
}
#[test]
fn test_get_filename_edge_cases() {
assert_eq!(get_filename("/path/to/file.log"), "file.log");
assert_eq!(get_filename("file.log"), "file.log");
assert_eq!(get_filename("/"), "unknown");
assert_eq!(get_filename(""), "unknown");
assert_eq!(get_filename("/path/to/"), "to");
}
#[test]
fn test_is_symlink() {
let temp_file = NamedTempFile::new().unwrap();
assert!(!is_symlink(temp_file.path()));
assert!(!is_symlink("/nonexistent/file.log"));
}
#[test]
fn test_resolve_symlink() {
let temp_file = NamedTempFile::new().unwrap();
let result = resolve_symlink(temp_file.path());
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Failed to read symlink"));
let result = resolve_symlink("/nonexistent/file.log");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Failed to read symlink"));
}
#[test]
fn test_read_file_from_end_coverage_line_11() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "Line 1").unwrap();
writeln!(temp_file, "Line 2").unwrap();
writeln!(temp_file, "Line 3").unwrap();
temp_file.flush().unwrap();
let result = read_file_from_end(temp_file.path(), 1024);
assert!(result.is_ok());
let lines = result.unwrap();
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], "Line 1");
assert_eq!(lines[1], "Line 2");
assert_eq!(lines[2], "Line 3");
}
#[test]
fn test_get_file_size_coverage_line_32() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "Test content").unwrap();
temp_file.flush().unwrap();
let result = get_file_size(temp_file.path());
assert!(result.is_ok());
let size = result.unwrap();
assert!(size > 0);
}
#[test]
fn test_is_file_rotated_coverage_line_38() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, "Test content").unwrap();
temp_file.flush().unwrap();
let result = is_file_rotated(temp_file.path(), 1000); assert!(result.is_ok());
let is_rotated = result.unwrap();
assert!(is_rotated); }
#[test]
fn test_format_file_size_coverage_line_68() {
let result = format_file_size(1536); assert_eq!(result, "1.5 KB");
let result = format_file_size(1048576); assert_eq!(result, "1.0 MB");
}
#[test]
fn test_resolve_symlink_coverage_line_112() {
let result = resolve_symlink("/nonexistent/file.log");
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Failed to read symlink"));
}
}