log_watcher/
utils.rs

1use anyhow::{Context, Result};
2use std::fs::File;
3use std::io::{BufRead, BufReader};
4use std::path::Path;
5
6/// Read all lines from a file
7pub fn read_file_from_end<P: AsRef<Path>>(path: P, _buffer_size: usize) -> Result<Vec<String>> {
8    let file = File::open(&path)
9        .with_context(|| format!("Failed to open file: {}", path.as_ref().display()))?;
10
11    let reader = BufReader::new(file);
12    let mut lines = Vec::new();
13
14    for line_result in reader.lines() {
15        let line = line_result?;
16        if !line.trim().is_empty() {
17            lines.push(line.trim().to_string());
18        }
19    }
20
21    Ok(lines)
22}
23
24/// Check if a file exists and is readable
25pub fn is_file_readable<P: AsRef<Path>>(path: P) -> bool {
26    File::open(path).is_ok()
27}
28
29/// Get file size
30pub fn get_file_size<P: AsRef<Path>>(path: P) -> Result<u64> {
31    let metadata = std::fs::metadata(path).with_context(|| "Failed to get file metadata")?;
32    Ok(metadata.len())
33}
34
35/// Check if a file has been rotated (size decreased)
36pub fn is_file_rotated<P: AsRef<Path>>(path: P, previous_size: u64) -> Result<bool> {
37    let current_size = get_file_size(path)?;
38    Ok(current_size < previous_size)
39}
40
41/// Validate that all files exist and are readable
42pub fn validate_files<P: AsRef<Path> + Clone>(files: &[P]) -> Result<Vec<P>> {
43    let mut valid_files = Vec::new();
44    let mut errors = Vec::new();
45
46    for file in files {
47        if is_file_readable(file) {
48            valid_files.push(file.clone());
49        } else {
50            errors.push(format!("File not readable: {}", file.as_ref().display()));
51        }
52    }
53
54    if valid_files.is_empty() {
55        return Err(anyhow::anyhow!(
56            "No valid files to watch: {}",
57            errors.join(", ")
58        ));
59    }
60
61    if !errors.is_empty() {
62        eprintln!(
63            "Warning: Some files are not accessible: {}",
64            errors.join(", ")
65        );
66    }
67
68    Ok(valid_files)
69}
70
71/// Format file size in human-readable format
72pub fn format_file_size(size: u64) -> String {
73    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
74    let mut size = size as f64;
75    let mut unit_index = 0;
76
77    while size >= 1024.0 && unit_index < UNITS.len() - 1 {
78        size /= 1024.0;
79        unit_index += 1;
80    }
81
82    if unit_index == 0 {
83        format!("{} {}", size as u64, UNITS[unit_index])
84    } else {
85        format!("{:.1} {}", size, UNITS[unit_index])
86    }
87}
88
89/// Get filename from path
90pub fn get_filename<P: AsRef<Path>>(path: P) -> String {
91    path.as_ref()
92        .file_name()
93        .and_then(|name| name.to_str())
94        .unwrap_or("unknown")
95        .to_string()
96}
97
98/// Check if a path is a symlink
99pub fn is_symlink<P: AsRef<Path>>(path: P) -> bool {
100    path.as_ref()
101        .symlink_metadata()
102        .map(|metadata| metadata.file_type().is_symlink())
103        .unwrap_or(false)
104}
105
106/// Resolve symlink to actual path
107pub fn resolve_symlink<P: AsRef<Path>>(path: P) -> Result<std::path::PathBuf> {
108    let resolved = path
109        .as_ref()
110        .read_link()
111        .with_context(|| format!("Failed to read symlink: {}", path.as_ref().display()))?;
112    Ok(resolved)
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use std::io::Write;
119    use tempfile::NamedTempFile;
120
121    #[test]
122    fn test_format_file_size() {
123        assert_eq!(format_file_size(1024), "1.0 KB");
124        assert_eq!(format_file_size(1536), "1.5 KB");
125        assert_eq!(format_file_size(1048576), "1.0 MB");
126        assert_eq!(format_file_size(1023), "1023 B");
127    }
128
129    #[test]
130    fn test_get_filename() {
131        assert_eq!(get_filename("/path/to/file.log"), "file.log");
132        assert_eq!(get_filename("file.log"), "file.log");
133        assert_eq!(get_filename("/"), "unknown");
134    }
135
136    #[test]
137    fn test_validate_files() {
138        // Create a temporary file
139        let temp_file = NamedTempFile::new().unwrap();
140        let temp_path = temp_file.path();
141
142        // Test with valid file
143        let valid_files = vec![temp_path];
144        let result = validate_files(&valid_files);
145        assert!(result.is_ok());
146        assert_eq!(result.unwrap().len(), 1);
147
148        // Test with invalid file
149        let invalid_files = vec![std::path::Path::new("/nonexistent/file.log")];
150        let result = validate_files(&invalid_files);
151        assert!(result.is_err());
152    }
153
154    #[test]
155    fn test_read_file_from_end() {
156        let mut temp_file = NamedTempFile::new().unwrap();
157        writeln!(temp_file, "line 1").unwrap();
158        writeln!(temp_file, "line 2").unwrap();
159        writeln!(temp_file, "line 3").unwrap();
160        temp_file.flush().unwrap();
161
162        let lines = read_file_from_end(temp_file.path(), 1024).unwrap();
163        assert_eq!(lines.len(), 3);
164        assert_eq!(lines[0], "line 1");
165        assert_eq!(lines[1], "line 2");
166        assert_eq!(lines[2], "line 3");
167    }
168}