1use anyhow::{Context, Result};
2use std::fs::File;
3use std::io::{BufRead, BufReader};
4use std::path::Path;
5
6pub 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
24pub fn is_file_readable<P: AsRef<Path>>(path: P) -> bool {
26 File::open(path).is_ok()
27}
28
29pub 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
35pub 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
41pub 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
71pub 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
89pub 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
98pub 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
106pub 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 let temp_file = NamedTempFile::new().unwrap();
140 let temp_path = temp_file.path();
141
142 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 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
169 #[test]
170 fn test_read_file_from_end_with_empty_lines() {
171 let mut temp_file = NamedTempFile::new().unwrap();
172 writeln!(temp_file, "line 1").unwrap();
173 writeln!(temp_file).unwrap(); writeln!(temp_file, " ").unwrap(); writeln!(temp_file, "line 2").unwrap();
176 temp_file.flush().unwrap();
177
178 let lines = read_file_from_end(temp_file.path(), 1024).unwrap();
179 assert_eq!(lines.len(), 2); assert_eq!(lines[0], "line 1");
181 assert_eq!(lines[1], "line 2");
182 }
183
184 #[test]
185 fn test_read_file_from_end_with_nonexistent_file() {
186 let result = read_file_from_end("/nonexistent/file.log", 1024);
187 assert!(result.is_err());
188 assert!(result
189 .unwrap_err()
190 .to_string()
191 .contains("Failed to open file"));
192 }
193
194 #[test]
195 fn test_is_file_readable() {
196 let temp_file = NamedTempFile::new().unwrap();
197 assert!(is_file_readable(temp_file.path()));
198
199 assert!(!is_file_readable("/nonexistent/file.log"));
200 }
201
202 #[test]
203 fn test_get_file_size() {
204 let mut temp_file = NamedTempFile::new().unwrap();
205 writeln!(temp_file, "test content").unwrap();
206 temp_file.flush().unwrap();
207
208 let size = get_file_size(temp_file.path()).unwrap();
209 assert!(size > 0);
210 }
211
212 #[test]
213 fn test_get_file_size_nonexistent() {
214 let result = get_file_size("/nonexistent/file.log");
215 assert!(result.is_err());
216 assert!(result
217 .unwrap_err()
218 .to_string()
219 .contains("Failed to get file metadata"));
220 }
221
222 #[test]
223 fn test_is_file_rotated() {
224 let mut temp_file = NamedTempFile::new().unwrap();
225 writeln!(temp_file, "initial content").unwrap();
226 temp_file.flush().unwrap();
227
228 let initial_size = get_file_size(temp_file.path()).unwrap();
229
230 writeln!(temp_file, "more content").unwrap();
232 temp_file.flush().unwrap();
233
234 assert!(!is_file_rotated(temp_file.path(), initial_size).unwrap());
236
237 temp_file.as_file_mut().set_len(0).unwrap();
239 temp_file.flush().unwrap();
240
241 assert!(is_file_rotated(temp_file.path(), initial_size).unwrap());
243 }
244
245 #[test]
246 fn test_validate_files_with_mixed_validity() {
247 let temp_file = NamedTempFile::new().unwrap();
248 let temp_path = temp_file.path();
249
250 let files = vec![
251 temp_path,
252 std::path::Path::new("/nonexistent/file1.log"),
253 std::path::Path::new("/nonexistent/file2.log"),
254 ];
255
256 let result = validate_files(&files);
257 assert!(result.is_ok());
258
259 let valid_files = result.unwrap();
260 assert_eq!(valid_files.len(), 1);
261 assert_eq!(valid_files[0], temp_path);
262 }
263
264 #[test]
265 fn test_validate_files_all_invalid() {
266 let files = vec![
267 std::path::Path::new("/nonexistent/file1.log"),
268 std::path::Path::new("/nonexistent/file2.log"),
269 ];
270
271 let result = validate_files(&files);
272 assert!(result.is_err());
273 assert!(result
274 .unwrap_err()
275 .to_string()
276 .contains("No valid files to watch"));
277 }
278
279 #[test]
280 fn test_format_file_size_edge_cases() {
281 assert_eq!(format_file_size(0), "0 B");
283 assert_eq!(format_file_size(1), "1 B");
284 assert_eq!(format_file_size(1023), "1023 B");
285
286 assert_eq!(format_file_size(1024), "1.0 KB");
288 assert_eq!(format_file_size(1536), "1.5 KB");
289 assert_eq!(format_file_size(2048), "2.0 KB");
290
291 assert_eq!(format_file_size(1048576), "1.0 MB");
293 assert_eq!(format_file_size(1572864), "1.5 MB");
294
295 assert_eq!(format_file_size(1073741824), "1.0 GB");
297
298 assert_eq!(format_file_size(1099511627776), "1.0 TB");
300 }
301
302 #[test]
303 fn test_get_filename_edge_cases() {
304 assert_eq!(get_filename("/path/to/file.log"), "file.log");
305 assert_eq!(get_filename("file.log"), "file.log");
306 assert_eq!(get_filename("/"), "unknown");
307 assert_eq!(get_filename(""), "unknown");
308 assert_eq!(get_filename("/path/to/"), "to");
310 }
311
312 #[test]
313 fn test_is_symlink() {
314 let temp_file = NamedTempFile::new().unwrap();
316 assert!(!is_symlink(temp_file.path()));
317
318 assert!(!is_symlink("/nonexistent/file.log"));
320 }
321
322 #[test]
323 fn test_resolve_symlink() {
324 let temp_file = NamedTempFile::new().unwrap();
326 let result = resolve_symlink(temp_file.path());
327 assert!(result.is_err());
328 assert!(result
329 .unwrap_err()
330 .to_string()
331 .contains("Failed to read symlink"));
332
333 let result = resolve_symlink("/nonexistent/file.log");
335 assert!(result.is_err());
336 assert!(result
337 .unwrap_err()
338 .to_string()
339 .contains("Failed to read symlink"));
340 }
341}