1use super::now;
7use crate::SuffixInfo;
8use chrono::{format::ParseErrorKind, offset::Local, Duration, NaiveDateTime};
9use std::{
10 cmp::Ordering,
11 collections::BTreeSet,
12 io,
13 path::{Path, PathBuf},
14};
15
16pub trait Representation: Ord + ToString + Eq + Clone + std::fmt::Debug {
19 fn to_path(&self, basepath: &Path) -> PathBuf {
21 PathBuf::from(format!("{}.{}", basepath.display(), self.to_string()))
22 }
23}
24
25pub trait SuffixScheme {
27 type Repr: Representation;
30
31 fn rotate_file(
44 &mut self,
45 basepath: &Path,
46 newest_suffix: Option<&Self::Repr>,
47 suffix: &Option<Self::Repr>,
48 ) -> io::Result<Self::Repr>;
49
50 fn parse(&self, suffix: &str) -> Option<Self::Repr>;
52
53 fn too_old(&self, suffix: &Self::Repr, file_number: usize) -> bool;
57
58 fn scan_suffixes(&self, basepath: &Path) -> BTreeSet<SuffixInfo<Self::Repr>> {
62 let mut suffixes = BTreeSet::new();
63 let filename_prefix = basepath
64 .file_name()
65 .expect("basepath.file_name()")
66 .to_string_lossy();
67
68 let basepath = if basepath.is_relative() {
71 let mut path = std::env::current_dir().unwrap();
72 path.push(basepath);
73 path
74 } else {
75 basepath.to_path_buf()
76 };
77
78 let parent = basepath.parent().unwrap();
79
80 let filenames = std::fs::read_dir(parent)
81 .unwrap()
82 .filter_map(|entry| entry.ok())
83 .filter(|entry| entry.path().is_file())
84 .map(|entry| entry.file_name());
85 for filename in filenames {
86 let filename = filename.to_string_lossy();
87 if !filename.starts_with(&*filename_prefix) {
88 continue;
89 }
90 let (filename, compressed) = prepare_filename(&*filename);
91 let suffix_str = filename.strip_prefix(&format!("{}.", filename_prefix));
92 if let Some(suffix) = suffix_str.and_then(|s| self.parse(s)) {
93 suffixes.insert(SuffixInfo { suffix, compressed });
94 }
95 }
96 suffixes
97 }
98}
99fn prepare_filename(path: &str) -> (&str, bool) {
100 path.strip_suffix(".gz")
101 .map(|x| (x, true))
102 .unwrap_or((path, false))
103}
104
105pub struct AppendCount {
108 max_files: usize,
109}
110
111impl AppendCount {
112 pub fn new(max_files: usize) -> Self {
117 Self { max_files }
118 }
119}
120
121impl Representation for usize {}
122impl SuffixScheme for AppendCount {
123 type Repr = usize;
124 fn rotate_file(
125 &mut self,
126 _basepath: &Path,
127 _: Option<&usize>,
128 suffix: &Option<usize>,
129 ) -> io::Result<usize> {
130 Ok(match suffix {
131 Some(suffix) => suffix + 1,
132 None => 1,
133 })
134 }
135 fn parse(&self, suffix: &str) -> Option<usize> {
136 suffix.parse::<usize>().ok()
137 }
138 fn too_old(&self, _suffix: &usize, file_number: usize) -> bool {
139 file_number >= self.max_files
140 }
141}
142
143pub enum DateFrom {
145 DateYesterday,
147 DateHourAgo,
149 Now,
151}
152
153pub struct AppendTimestamp {
160 pub format: &'static str,
162 pub file_limit: FileLimit,
164 pub date_from: DateFrom,
166}
167
168impl AppendTimestamp {
169 pub fn default(file_limit: FileLimit) -> Self {
171 Self {
172 format: "%Y%m%dT%H%M%S",
173 file_limit,
174 date_from: DateFrom::Now,
175 }
176 }
177 pub fn with_format(format: &'static str, file_limit: FileLimit, date_from: DateFrom) -> Self {
179 Self {
180 format,
181 file_limit,
182 date_from,
183 }
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct TimestampSuffix {
190 pub timestamp: String,
192 pub number: Option<usize>,
194}
195impl Representation for TimestampSuffix {}
196impl Ord for TimestampSuffix {
197 fn cmp(&self, other: &Self) -> Ordering {
198 match other.timestamp.cmp(&self.timestamp) {
201 Ordering::Equal => other.number.cmp(&self.number),
202 unequal => unequal,
203 }
204 }
205}
206impl PartialOrd for TimestampSuffix {
207 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
208 Some(self.cmp(other))
209 }
210}
211impl std::fmt::Display for TimestampSuffix {
212 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
213 match self.number {
214 Some(n) => write!(f, "{}.{}", self.timestamp, n),
215 None => write!(f, "{}", self.timestamp),
216 }
217 }
218}
219
220impl SuffixScheme for AppendTimestamp {
221 type Repr = TimestampSuffix;
222
223 fn rotate_file(
224 &mut self,
225 _basepath: &Path,
226 newest_suffix: Option<&TimestampSuffix>,
227 suffix: &Option<TimestampSuffix>,
228 ) -> io::Result<TimestampSuffix> {
229 assert!(suffix.is_none());
230 if suffix.is_none() {
231 let mut now = now();
232
233 match self.date_from {
234 DateFrom::DateYesterday => {
235 now = now - Duration::days(1);
236 }
237 DateFrom::DateHourAgo => {
238 now = now - Duration::hours(1);
239 }
240 _ => {}
241 };
242
243 let fmt_now = now.format(self.format).to_string();
244
245 let number = if let Some(newest_suffix) = newest_suffix {
246 if newest_suffix.timestamp == fmt_now {
247 Some(newest_suffix.number.unwrap_or(0) + 1)
248 } else {
249 None
250 }
251 } else {
252 None
253 };
254 Ok(TimestampSuffix {
255 timestamp: fmt_now,
256 number,
257 })
258 } else {
259 Err(io::Error::new(
262 io::ErrorKind::InvalidData,
263 "Critical error in file-rotate algorithm",
264 ))
265 }
266 }
267 fn parse(&self, suffix: &str) -> Option<Self::Repr> {
268 let (timestamp_str, n) = if let Some(dot) = suffix.find('.') {
269 if let Ok(n) = suffix[(dot + 1)..].parse::<usize>() {
270 (&suffix[..dot], Some(n))
271 } else {
272 return None;
273 }
274 } else {
275 (suffix, None)
276 };
277 let success = match NaiveDateTime::parse_from_str(timestamp_str, self.format) {
278 Ok(_) => true,
279 Err(e) => e.kind() == ParseErrorKind::NotEnough,
280 };
281 if success {
282 Some(TimestampSuffix {
283 timestamp: timestamp_str.to_string(),
284 number: n,
285 })
286 } else {
287 None
288 }
289 }
290 fn too_old(&self, suffix: &TimestampSuffix, file_number: usize) -> bool {
291 match self.file_limit {
292 FileLimit::MaxFiles(max_files) => file_number >= max_files,
293 FileLimit::Age(age) => {
294 let old_timestamp = (Local::now() - age).format(self.format).to_string();
295 suffix.timestamp < old_timestamp
296 }
297 FileLimit::Unlimited => false,
298 }
299 }
300}
301
302pub enum FileLimit {
304 MaxFiles(usize),
306 Age(Duration),
308 Unlimited,
310}
311
312#[cfg(test)]
313mod test {
314 use super::*;
315 use std::fs::File;
316 use tempfile::TempDir;
317 #[test]
318 fn timestamp_ordering() {
319 assert!(
320 TimestampSuffix {
321 timestamp: "2021".to_string(),
322 number: None
323 } < TimestampSuffix {
324 timestamp: "2020".to_string(),
325 number: None
326 }
327 );
328 assert!(
329 TimestampSuffix {
330 timestamp: "2021".to_string(),
331 number: Some(1)
332 } < TimestampSuffix {
333 timestamp: "2021".to_string(),
334 number: None
335 }
336 );
337 }
338
339 #[test]
340 fn timestamp_scan_suffixes_base_paths() {
341 let working_dir = TempDir::new().unwrap();
342 let working_dir = working_dir.path().join("dir");
343 let suffix_scheme = AppendTimestamp::default(FileLimit::Age(Duration::weeks(1)));
344
345 for relative_path in ["logs/log", "./log", "log", "../log", "../logs/log"] {
348 std::fs::create_dir_all(&working_dir).unwrap();
349 println!("Testing relative path: {}", relative_path);
350 let relative_path = Path::new(relative_path);
351
352 let log_file = working_dir.join(relative_path);
353 let log_dir = log_file.parent().unwrap();
354 std::fs::create_dir_all(log_dir).unwrap();
356
357 std::env::set_current_dir(&working_dir).unwrap();
359
360 File::create(working_dir.join(&relative_path)).unwrap();
362 let canonicalized = relative_path.canonicalize().unwrap();
363 let relative_dir = canonicalized.parent().unwrap();
364
365 File::create(relative_dir.join("log.20210911T121830")).unwrap();
366 File::create(relative_dir.join("log.20210911T121831.gz")).unwrap();
367
368 let paths = suffix_scheme.scan_suffixes(relative_path);
369 assert_eq!(paths.len(), 2);
370
371 std::env::set_current_dir("/").unwrap();
375
376 std::fs::remove_dir_all(&working_dir).unwrap();
378 }
379 }
380
381 #[test]
382 fn timestamp_scan_suffixes_formats() {
383 struct TestCase {
384 format: &'static str,
385 suffixes: &'static [&'static str],
386 incorrect_suffixes: &'static [&'static str],
387 }
388
389 let cases = [
390 TestCase {
391 format: "%Y%m%dT%H%M%S",
392 suffixes: &["20220201T101010", "20220202T101010"],
393 incorrect_suffixes: &["20220201T1010", "20220201T999999", "2022-02-02"],
394 },
395 TestCase {
396 format: "%Y-%m-%d",
397 suffixes: &["2022-02-01", "2022-02-02"],
398 incorrect_suffixes: &[
399 "abc",
400 "2022-99-99",
401 "2022-05",
402 "2022",
403 "20220202",
404 "2022-02-02T112233",
405 ],
406 },
407 ];
408
409 for (i, case) in cases.iter().enumerate() {
410 println!("Case {}", i);
411 let tmp_dir = TempDir::new().unwrap();
412 let dir = tmp_dir.path();
413 let log_path = dir.join("file");
414
415 for suffix in case.suffixes.iter().chain(case.incorrect_suffixes) {
416 File::create(dir.join(format!("file.{}", suffix))).unwrap();
417 }
418
419 let scheme = AppendTimestamp::with_format(
420 case.format,
421 FileLimit::MaxFiles(1),
422 DateFrom::DateYesterday,
423 );
424
425 let suffixes_set = scheme.scan_suffixes(&log_path);
427
428 let mut suffixes = suffixes_set
430 .into_iter()
431 .map(|x| x.suffix.to_string())
432 .collect::<Vec<_>>();
433 suffixes.sort_unstable();
434
435 let mut expected_suffixes = case.suffixes.to_vec();
436 expected_suffixes.sort_unstable();
437
438 assert_eq!(suffixes, case.suffixes);
439 println!("Passed\n");
440 }
441 }
442}