1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
pub use error::{FileRotationError, Result};
use std::path::{Path, PathBuf};
mod error;
pub struct FileRotation {
max_old_files: Option<usize>,
file: PathBuf,
}
impl FileRotation {
pub fn new(file: impl AsRef<Path>) -> Self {
Self {
file: file.as_ref().to_path_buf(),
max_old_files: None,
}
}
#[must_use]
pub fn max_old_files(mut self, max_old_files: usize) -> Self {
self.max_old_files = Some(max_old_files);
self
}
pub fn rotate(self) -> Result<()> {
let Self {
max_old_files,
file,
} = self;
let log_file = match file.extension() {
Some(_) => file,
None => file.with_extension("log"),
};
let log_file_name = log_file.file_name().unwrap();
let log_file_dir = log_file
.parent()
.and_then(|p| {
let dir = p.to_path_buf();
if dir.to_string_lossy().is_empty() {
None
} else {
Some(dir)
}
})
.unwrap_or_else(|| PathBuf::from("."));
let mut rotations = Vec::new();
for entry in std::fs::read_dir(&log_file_dir)? {
let entry = match entry {
Err(_) => continue,
Ok(entry) => entry,
};
let file_name = entry.file_name();
if file_name == log_file_name {
rotations.push((
entry,
log_file_name.to_string_lossy().replace(".log", ".1.log"),
));
continue;
}
let log_file_name = log_file_name.to_string_lossy();
let file_name = file_name.to_string_lossy();
let parts = file_name.split('.').collect::<Vec<_>>();
match parts[..] {
[prefix, n, ext] if !prefix.is_empty() && log_file_name.starts_with(prefix) => {
if let Ok(n) = n.parse::<usize>() {
let new_name = format!("{prefix}.{}.{ext}", n + 1);
rotations.push((entry, new_name));
}
}
_ => continue,
}
}
rotations.sort_by_key(|(_, new_name)| new_name.to_string());
if let Some(max_old_files) = max_old_files {
while rotations.len() > max_old_files {
if let Some((log_file, _)) = rotations.pop() {
if let Err(err) = std::fs::remove_file(log_file.path()) {
eprintln!(
"Rotating logs: cannot remove file {}: {err}",
log_file.path().display()
);
}
}
}
}
for (entry, new_file_name) in rotations.into_iter().rev() {
println!("renaming {:?} -> {new_file_name:?}", entry.path());
if let Err(err) = std::fs::rename(entry.path(), log_file_dir.join(new_file_name)) {
eprintln!("Error rotating log file {entry:?}: {err}");
}
}
Ok(())
}
}