1use std::{
3 fs::{self, File, OpenOptions},
4 io::{self, Write},
5 path::{Path, PathBuf},
6};
7
8pub enum RotationMode {
10 Bytes(usize),
12 Lines(usize),
14 BytesSurpassed(usize),
16}
17
18pub struct FileRotate {
20 basename: PathBuf,
21 count: usize,
22 file: Option<File>,
23 file_number: usize,
24 max_file_number: usize,
25 mode: RotationMode,
26}
27
28impl FileRotate {
29 pub fn open<P: AsRef<Path>>(
40 path: P,
41 rotation_mode: RotationMode,
42 max_file_number: usize,
43 ) -> anyhow::Result<Self> {
44 match rotation_mode {
45 RotationMode::Bytes(bytes) => {
46 assert!(bytes > 0);
47 }
48 RotationMode::Lines(lines) => {
49 assert!(lines > 0);
50 }
51 RotationMode::BytesSurpassed(bytes) => {
52 assert!(bytes > 0);
53 }
54 };
55
56 let path = path.as_ref();
57
58 let file = OpenOptions::new().create(true).append(true).open(path)?;
59
60 let file_size = file.metadata().map(|it| it.len()).unwrap_or_default();
61
62 let file_number = current_file_number(path)
63 .ok()
64 .unwrap_or_default()
65 .map(|it| it + 1)
66 .unwrap_or_default();
67
68 let count = match &rotation_mode {
69 RotationMode::Bytes(_) | RotationMode::BytesSurpassed(_) => file_size as usize,
70 RotationMode::Lines(_) => 0usize,
71 };
72
73 Ok(Self {
74 basename: path.to_path_buf(),
75 count,
76 file: Some(file),
77 file_number,
78 max_file_number,
79 mode: rotation_mode,
80 })
81 }
82
83 fn rotate(&mut self) -> io::Result<()> {
84 let mut path = self.basename.clone();
85 let new_file_name = format!(
86 "{}.{}",
87 path.file_name().unwrap().to_str().unwrap(),
88 self.file_number
89 );
90
91 let deleted = if self.file_number >= self.max_file_number {
92 Some(format!(
93 "{}.{}",
94 path.file_name().unwrap().to_str().unwrap(),
95 self.file_number - self.max_file_number
96 ))
97 } else {
98 None
99 };
100
101 path.set_file_name(new_file_name);
102
103 let _ = self.file.take();
104
105 let _ = fs::rename(&self.basename, path);
106
107 self.file = Some(File::create(&self.basename)?);
108
109 if let Some(d) = deleted {
111 let mut to_be_deleted = self.basename.clone();
112 to_be_deleted.set_file_name(d);
113 fs::remove_file(to_be_deleted).ok();
114 }
115
116 self.file_number += 1;
117 self.count = 0;
118
119 Ok(())
120 }
121}
122
123impl Write for FileRotate {
124 fn write(&mut self, mut buf: &[u8]) -> io::Result<usize> {
125 let written = buf.len();
126 match self.mode {
127 RotationMode::Bytes(bytes) => {
128 while self.count + buf.len() > bytes {
129 let bytes_left = bytes - self.count;
130 if let Some(Err(err)) = self
131 .file
132 .as_mut()
133 .map(|file| file.write(&buf[..bytes_left]))
134 {
135 return Err(err);
136 }
137 self.rotate()?;
138 buf = &buf[bytes_left..];
139 }
140 self.count += buf.len();
141 if let Some(Err(err)) = self.file.as_mut().map(|file| file.write(buf)) {
142 return Err(err);
143 }
144 }
145 RotationMode::Lines(lines) => {
146 while let Some((idx, _)) = buf.iter().enumerate().find(|(_, byte)| *byte == &b'\n')
147 {
148 if let Some(Err(err)) =
149 self.file.as_mut().map(|file| file.write(&buf[..idx + 1]))
150 {
151 return Err(err);
152 }
153 self.count += 1;
154 buf = &buf[idx + 1..];
155 if self.count >= lines {
156 self.rotate()?;
157 }
158 }
159 if let Some(Err(err)) = self.file.as_mut().map(|file| file.write(buf)) {
160 return Err(err);
161 }
162 }
163 RotationMode::BytesSurpassed(bytes) => {
164 if let Some(Err(err)) = self.file.as_mut().map(|file| file.write(buf)) {
165 return Err(err);
166 }
167 self.count += buf.len();
168 if self.count > bytes {
169 self.rotate()?
170 }
171 }
172 }
173 Ok(written)
174 }
175
176 fn flush(&mut self) -> io::Result<()> {
177 if let Some(Err(err)) = self.file.as_mut().map(|file| file.flush()) {
178 Err(err)
179 } else {
180 Ok(())
181 }
182 }
183}
184
185fn current_file_number(path: &Path) -> anyhow::Result<Option<usize>> {
186 let filename = path.file_name().unwrap().to_str().unwrap();
187 let mut n = -1i64;
188 for it in fs::read_dir(path.parent().unwrap())? {
189 let ent = it?;
190 let p = ent.path();
191 if !p.is_file() {
192 continue;
193 }
194 if let Some(f) = p.file_name() {
195 if let Some(s) = f.to_str() {
196 if s.starts_with(filename) && s.ne(filename) {
197 let suffix = &s[filename.len()..];
198 if suffix.starts_with('.') && suffix.len() > 1 {
199 let numstr = &suffix[1..];
200 if let Ok(v) = numstr.parse::<i64>() {
201 if v > n {
202 n = v;
203 }
204 }
205 }
206 }
207 }
208 }
209 }
210 if n >= 0 {
211 Ok(Some(n as usize))
212 } else {
213 Ok(None)
214 }
215}