]> git.proxmox.com Git - proxmox-backup.git/blame - src/tools/logrotate.rs
clippy: fix/allow identity_op
[proxmox-backup.git] / src / tools / logrotate.rs
CommitLineData
8074d2b0
DC
1use std::path::{Path, PathBuf};
2use std::fs::{File, rename};
b6570abe 3use std::os::unix::io::{FromRawFd, IntoRawFd};
8074d2b0
DC
4use std::io::Read;
5
6use anyhow::{bail, Error};
7use nix::unistd;
8
e6ca9c32 9use proxmox::tools::fs::{CreateOptions, make_tmp_file};
8074d2b0
DC
10
11/// Used for rotating log files and iterating over them
12pub struct LogRotate {
13 base_path: PathBuf,
14 compress: bool,
15}
16
17impl 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
8b4f4d9e
DC
49 fn compress(source_path: &PathBuf, target_path: &PathBuf, options: &CreateOptions) -> Result<(), Error> {
50 let mut source = File::open(source_path)?;
51 let (fd, tmp_path) = make_tmp_file(target_path, options.clone())?;
b6570abe 52 let target = unsafe { File::from_raw_fd(fd.into_raw_fd()) };
7827e3b9
TL
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);
8b4f4d9e 63 bail!("zstd encoding failed for file {:?} - {}", target_path, err);
7827e3b9
TL
64 }
65
66 if let Err(err) = encoder.finish() {
67 let _ = unistd::unlink(&tmp_path);
8b4f4d9e 68 bail!("zstd finish failed for file {:?} - {}", target_path, err);
7827e3b9
TL
69 }
70
8b4f4d9e 71 if let Err(err) = rename(&tmp_path, target_path) {
7827e3b9 72 let _ = unistd::unlink(&tmp_path);
8b4f4d9e 73 bail!("rename failed for file {:?} - {}", target_path, err);
7827e3b9 74 }
8b4f4d9e
DC
75
76 if let Err(err) = unistd::unlink(source_path) {
77 bail!("unlink failed for file {:?} - {}", source_path, err);
78 }
79
7827e3b9
TL
80 Ok(())
81 }
82
8074d2b0
DC
83 /// Rotates the files up to 'max_files'
84 /// if the 'compress' option was given it will compress the newest file
85 ///
86 /// e.g. rotates
87 /// foo.2.zst => foo.3.zst
8b4f4d9e
DC
88 /// foo.1 => foo.2.zst
89 /// foo => foo.1
95ade8fd 90 pub fn do_rotate(&mut self, options: CreateOptions, max_files: Option<usize>) -> Result<(), Error> {
8074d2b0
DC
91 let mut filenames: Vec<PathBuf> = self.file_names().collect();
92 if filenames.is_empty() {
93 return Ok(()); // no file means nothing to rotate
94 }
be99df27 95 let count = filenames.len() + 1;
8074d2b0
DC
96
97 let mut next_filename = self.base_path.clone().canonicalize()?.into_os_string();
8b4f4d9e 98 next_filename.push(format!(".{}", filenames.len()));
be99df27 99 if self.compress && count > 2 {
3aade171
TL
100 next_filename.push(".zst");
101 }
8074d2b0
DC
102
103 filenames.push(PathBuf::from(next_filename));
8074d2b0 104
9e870b5f 105 for i in (0..count-1).rev() {
3aade171 106 if self.compress
81281d04 107 && filenames[i].extension() != Some(std::ffi::OsStr::new("zst"))
e062ebbc 108 && filenames[i+1].extension() == Some(std::ffi::OsStr::new("zst"))
3aade171
TL
109 {
110 Self::compress(&filenames[i], &filenames[i+1], &options)?;
111 } else {
112 rename(&filenames[i], &filenames[i+1])?;
8074d2b0 113 }
8074d2b0
DC
114 }
115
8074d2b0 116 if let Some(max_files) = max_files {
8074d2b0
DC
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 }
95ade8fd
TL
126
127 pub fn rotate(
128 &mut self,
129 max_size: u64,
130 options: Option<CreateOptions>,
131 max_files: Option<usize>
132 ) -> Result<bool, Error> {
133
134 let options = match options {
135 Some(options) => options,
136 None => {
137 let backup_user = crate::backup::backup_user()?;
138 CreateOptions::new().owner(backup_user.uid).group(backup_user.gid)
139 },
140 };
141
142 let metadata = match self.base_path.metadata() {
143 Ok(metadata) => metadata,
144 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false),
145 Err(err) => bail!("unable to open task archive - {}", err),
146 };
147
148 if metadata.len() > max_size {
149 self.do_rotate(options, max_files)?;
150 Ok(true)
151 } else {
152 Ok(false)
153 }
154 }
8074d2b0
DC
155}
156
157/// Iterator over logrotated file names
158pub struct LogRotateFileNames {
159 base_path: PathBuf,
160 count: usize,
161 compress: bool,
162}
163
164impl Iterator for LogRotateFileNames {
165 type Item = PathBuf;
166
167 fn next(&mut self) -> Option<Self::Item> {
168 if self.count > 0 {
169 let mut path: std::ffi::OsString = self.base_path.clone().into();
170
171 path.push(format!(".{}", self.count));
172 self.count += 1;
173
174 if Path::new(&path).is_file() {
175 Some(path.into())
176 } else if self.compress {
177 path.push(".zst");
178 if Path::new(&path).is_file() {
179 Some(path.into())
180 } else {
181 None
182 }
183 } else {
184 None
185 }
186 } else if self.base_path.is_file() {
187 self.count += 1;
188 Some(self.base_path.to_path_buf())
189 } else {
190 None
191 }
192 }
193}
194
195/// Iterator over logrotated files by returning a boxed reader
196pub struct LogRotateFiles {
197 file_names: LogRotateFileNames,
198}
199
200impl Iterator for LogRotateFiles {
201 type Item = Box<dyn Read + Send>;
202
203 fn next(&mut self) -> Option<Self::Item> {
204 let filename = self.file_names.next()?;
205 let file = File::open(&filename).ok()?;
206
e062ebbc 207 if filename.extension() == Some(std::ffi::OsStr::new("zst")) {
8074d2b0
DC
208 let encoder = zstd::stream::read::Decoder::new(file).ok()?;
209 return Some(Box::new(encoder));
210 }
211
212 Some(Box::new(file))
213 }
214}