capybara_util/
rotate.rs

1/// https://github.com/BourgondAries/file-rotate](https://github.com/BourgondAries/file-rotate
2use std::{
3    fs::{self, File, OpenOptions},
4    io::{self, Write},
5    path::{Path, PathBuf},
6};
7
8/// Condition on which a file is rotated.
9pub enum RotationMode {
10    /// Cut the log at the exact size in bytes.
11    Bytes(usize),
12    /// Cut the log file at line breaks.
13    Lines(usize),
14    /// Cut the log file after surpassing size in bytes (but having written a complete buffer from a write call.)
15    BytesSurpassed(usize),
16}
17
18/// The main writer used for rotating logs.
19pub 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    /// Create a new [FileRotate].
30    ///
31    /// The basename of the `path` is used to create new log files by appending an extension of the
32    /// form `.N`, where N is `0..=max_file_number`.
33    ///
34    /// `rotation_mode` specifies the limits for rotating a file.
35    ///
36    /// # Panics
37    ///
38    /// Panics if `bytes == 0` or `lines == 0`.
39    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        // 删除旧日志
110        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}