]> git.proxmox.com Git - proxmox-backup.git/blob - src/tools/logrotate.rs
log rotate: do NOT compress first rotation
[proxmox-backup.git] / src / tools / logrotate.rs
1 use std::path::{Path, PathBuf};
2 use std::fs::{File, rename};
3 use std::os::unix::io::FromRawFd;
4 use std::io::Read;
5
6 use anyhow::{bail, Error};
7 use nix::unistd;
8
9 use proxmox::tools::fs::{CreateOptions, make_tmp_file};
10
11 /// Used for rotating log files and iterating over them
12 pub struct LogRotate {
13 base_path: PathBuf,
14 compress: bool,
15 }
16
17 impl LogRotate {
18 /// Creates a new instance if the path given is a valid file name
19 /// (iow. does not end with ..)
20 /// 'compress' decides if compresses files will be created on
21 /// rotation, and if it will search '.zst' files when iterating
22 pub fn new<P: AsRef<Path>>(path: P, compress: bool) -> Option<Self> {
23 if path.as_ref().file_name().is_some() {
24 Some(Self {
25 base_path: path.as_ref().to_path_buf(),
26 compress,
27 })
28 } else {
29 None
30 }
31 }
32
33 /// Returns an iterator over the logrotated file names that exist
34 pub fn file_names(&self) -> LogRotateFileNames {
35 LogRotateFileNames {
36 base_path: self.base_path.clone(),
37 count: 0,
38 compress: self.compress
39 }
40 }
41
42 /// Returns an iterator over the logrotated file handles
43 pub fn files(&self) -> LogRotateFiles {
44 LogRotateFiles {
45 file_names: self.file_names(),
46 }
47 }
48
49 fn compress(file: &PathBuf, options: &CreateOptions) -> Result<(), Error> {
50 let mut source = File::open(file)?;
51 let (fd, tmp_path) = make_tmp_file(file, options.clone())?;
52 let target = unsafe { File::from_raw_fd(fd) };
53 let mut encoder = match zstd::stream::write::Encoder::new(target, 0) {
54 Ok(encoder) => encoder,
55 Err(err) => {
56 let _ = unistd::unlink(&tmp_path);
57 bail!("creating zstd encoder failed - {}", err);
58 }
59 };
60
61 if let Err(err) = std::io::copy(&mut source, &mut encoder) {
62 let _ = unistd::unlink(&tmp_path);
63 bail!("zstd encoding failed for file {:?} - {}", file, err);
64 }
65
66 if let Err(err) = encoder.finish() {
67 let _ = unistd::unlink(&tmp_path);
68 bail!("zstd finish failed for file {:?} - {}", file, err);
69 }
70
71 if let Err(err) = rename(&tmp_path, file) {
72 let _ = unistd::unlink(&tmp_path);
73 bail!("rename failed for file {:?} - {}", file, err);
74 }
75 Ok(())
76 }
77
78 /// Rotates the files up to 'max_files'
79 /// if the 'compress' option was given it will compress the newest file
80 ///
81 /// e.g. rotates
82 /// foo.2.zst => foo.3.zst
83 /// foo.1.zst => foo.2.zst
84 /// foo => foo.1.zst
85 /// => foo
86 pub fn rotate(&mut self, options: CreateOptions, max_files: Option<usize>) -> Result<(), Error> {
87 let mut filenames: Vec<PathBuf> = self.file_names().collect();
88 if filenames.is_empty() {
89 return Ok(()); // no file means nothing to rotate
90 }
91
92 let mut next_filename = self.base_path.clone().canonicalize()?.into_os_string();
93
94 if self.compress {
95 next_filename.push(format!(".{}.zst", filenames.len()));
96 } else {
97 next_filename.push(format!(".{}", filenames.len()));
98 }
99
100 filenames.push(PathBuf::from(next_filename));
101 let count = filenames.len();
102
103 for i in (0..count-1).rev() {
104 rename(&filenames[i], &filenames[i+1])?;
105 }
106
107 if self.compress {
108 for i in 2..count-1 {
109 if filenames[i].extension().unwrap_or(std::ffi::OsStr::new("")) != "zst" {
110 Self::compress(&filenames[i], &options)?;
111 }
112 }
113 }
114
115 if let Some(max_files) = max_files {
116 // delete all files > max_files
117 for file in filenames.iter().skip(max_files) {
118 if let Err(err) = unistd::unlink(file) {
119 eprintln!("could not remove {:?}: {}", &file, err);
120 }
121 }
122 }
123
124 Ok(())
125 }
126 }
127
128 /// Iterator over logrotated file names
129 pub struct LogRotateFileNames {
130 base_path: PathBuf,
131 count: usize,
132 compress: bool,
133 }
134
135 impl Iterator for LogRotateFileNames {
136 type Item = PathBuf;
137
138 fn next(&mut self) -> Option<Self::Item> {
139 if self.count > 0 {
140 let mut path: std::ffi::OsString = self.base_path.clone().into();
141
142 path.push(format!(".{}", self.count));
143 self.count += 1;
144
145 if Path::new(&path).is_file() {
146 Some(path.into())
147 } else if self.compress {
148 path.push(".zst");
149 if Path::new(&path).is_file() {
150 Some(path.into())
151 } else {
152 None
153 }
154 } else {
155 None
156 }
157 } else if self.base_path.is_file() {
158 self.count += 1;
159 Some(self.base_path.to_path_buf())
160 } else {
161 None
162 }
163 }
164 }
165
166 /// Iterator over logrotated files by returning a boxed reader
167 pub struct LogRotateFiles {
168 file_names: LogRotateFileNames,
169 }
170
171 impl Iterator for LogRotateFiles {
172 type Item = Box<dyn Read + Send>;
173
174 fn next(&mut self) -> Option<Self::Item> {
175 let filename = self.file_names.next()?;
176 let file = File::open(&filename).ok()?;
177
178 if filename.extension().unwrap_or(std::ffi::OsStr::new("")) == "zst" {
179 let encoder = zstd::stream::read::Decoder::new(file).ok()?;
180 return Some(Box::new(encoder));
181 }
182
183 Some(Box::new(file))
184 }
185 }