]> git.proxmox.com Git - proxmox-backup.git/blob - pbs-tools/src/logrotate.rs
split proxmox-file-restore into its own crate
[proxmox-backup.git] / pbs-tools / src / logrotate.rs
1 use std::path::{Path, PathBuf};
2 use std::fs::{File, rename};
3 use std::os::unix::io::{FromRawFd, IntoRawFd};
4 use std::io::Read;
5
6 use anyhow::{bail, format_err, 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 /// User logs should be reowned to.
17 owner: Option<String>,
18 }
19
20 impl LogRotate {
21 /// Creates a new instance if the path given is a valid file name (iow. does not end with ..)
22 /// 'compress' decides if compresses files will be created on rotation, and if it will search
23 /// '.zst' files when iterating
24 ///
25 /// By default, newly created files will be owned by the backup user. See [`new_with_user`] for
26 /// a way to opt out of this behavior.
27 pub fn new<P: AsRef<Path>>(
28 path: P,
29 compress: bool,
30 ) -> Option<Self> {
31 Self::new_with_user(path, compress, Some(pbs_buildcfg::BACKUP_USER_NAME.to_owned()))
32 }
33
34 /// See [`new`]. Additionally this also takes a user which should by default be used to reown
35 /// new files to.
36 pub fn new_with_user<P: AsRef<Path>>(
37 path: P,
38 compress: bool,
39 owner: Option<String>,
40 ) -> Option<Self> {
41 if path.as_ref().file_name().is_some() {
42 Some(Self {
43 base_path: path.as_ref().to_path_buf(),
44 compress,
45 owner,
46 })
47 } else {
48 None
49 }
50 }
51
52 /// Returns an iterator over the logrotated file names that exist
53 pub fn file_names(&self) -> LogRotateFileNames {
54 LogRotateFileNames {
55 base_path: self.base_path.clone(),
56 count: 0,
57 compress: self.compress
58 }
59 }
60
61 /// Returns an iterator over the logrotated file handles
62 pub fn files(&self) -> LogRotateFiles {
63 LogRotateFiles {
64 file_names: self.file_names(),
65 }
66 }
67
68 fn compress(source_path: &PathBuf, target_path: &PathBuf, options: &CreateOptions) -> Result<(), Error> {
69 let mut source = File::open(source_path)?;
70 let (fd, tmp_path) = make_tmp_file(target_path, options.clone())?;
71 let target = unsafe { File::from_raw_fd(fd.into_raw_fd()) };
72 let mut encoder = match zstd::stream::write::Encoder::new(target, 0) {
73 Ok(encoder) => encoder,
74 Err(err) => {
75 let _ = unistd::unlink(&tmp_path);
76 bail!("creating zstd encoder failed - {}", err);
77 }
78 };
79
80 if let Err(err) = std::io::copy(&mut source, &mut encoder) {
81 let _ = unistd::unlink(&tmp_path);
82 bail!("zstd encoding failed for file {:?} - {}", target_path, err);
83 }
84
85 if let Err(err) = encoder.finish() {
86 let _ = unistd::unlink(&tmp_path);
87 bail!("zstd finish failed for file {:?} - {}", target_path, err);
88 }
89
90 if let Err(err) = rename(&tmp_path, target_path) {
91 let _ = unistd::unlink(&tmp_path);
92 bail!("rename failed for file {:?} - {}", target_path, err);
93 }
94
95 if let Err(err) = unistd::unlink(source_path) {
96 bail!("unlink failed for file {:?} - {}", source_path, err);
97 }
98
99 Ok(())
100 }
101
102 /// Rotates the files up to 'max_files'
103 /// if the 'compress' option was given it will compress the newest file
104 ///
105 /// e.g. rotates
106 /// foo.2.zst => foo.3.zst
107 /// foo.1 => foo.2.zst
108 /// foo => foo.1
109 pub fn do_rotate(&mut self, options: CreateOptions, max_files: Option<usize>) -> Result<(), Error> {
110 let mut filenames: Vec<PathBuf> = self.file_names().collect();
111 if filenames.is_empty() {
112 return Ok(()); // no file means nothing to rotate
113 }
114 let count = filenames.len() + 1;
115
116 let mut next_filename = self.base_path.clone().canonicalize()?.into_os_string();
117 next_filename.push(format!(".{}", filenames.len()));
118 if self.compress && count > 2 {
119 next_filename.push(".zst");
120 }
121
122 filenames.push(PathBuf::from(next_filename));
123
124 for i in (0..count-1).rev() {
125 if self.compress
126 && filenames[i].extension() != Some(std::ffi::OsStr::new("zst"))
127 && filenames[i+1].extension() == Some(std::ffi::OsStr::new("zst"))
128 {
129 Self::compress(&filenames[i], &filenames[i+1], &options)?;
130 } else {
131 rename(&filenames[i], &filenames[i+1])?;
132 }
133 }
134
135 if let Some(max_files) = max_files {
136 for file in filenames.iter().skip(max_files) {
137 if let Err(err) = unistd::unlink(file) {
138 eprintln!("could not remove {:?}: {}", &file, err);
139 }
140 }
141 }
142
143 Ok(())
144 }
145
146 pub fn rotate(
147 &mut self,
148 max_size: u64,
149 options: Option<CreateOptions>,
150 max_files: Option<usize>
151 ) -> Result<bool, Error> {
152
153 let options = match options {
154 Some(options) => options,
155 None => match self.owner.as_deref() {
156 Some(owner) => {
157 let user = crate::sys::query_user(owner)?
158 .ok_or_else(|| {
159 format_err!("failed to lookup owning user '{}' for logs", owner)
160 })?;
161 CreateOptions::new().owner(user.uid).group(user.gid)
162 }
163 None => CreateOptions::new(),
164 }
165 };
166
167 let metadata = match self.base_path.metadata() {
168 Ok(metadata) => metadata,
169 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false),
170 Err(err) => bail!("unable to open task archive - {}", err),
171 };
172
173 if metadata.len() > max_size {
174 self.do_rotate(options, max_files)?;
175 Ok(true)
176 } else {
177 Ok(false)
178 }
179 }
180 }
181
182 /// Iterator over logrotated file names
183 pub struct LogRotateFileNames {
184 base_path: PathBuf,
185 count: usize,
186 compress: bool,
187 }
188
189 impl Iterator for LogRotateFileNames {
190 type Item = PathBuf;
191
192 fn next(&mut self) -> Option<Self::Item> {
193 if self.count > 0 {
194 let mut path: std::ffi::OsString = self.base_path.clone().into();
195
196 path.push(format!(".{}", self.count));
197 self.count += 1;
198
199 if Path::new(&path).is_file() {
200 Some(path.into())
201 } else if self.compress {
202 path.push(".zst");
203 if Path::new(&path).is_file() {
204 Some(path.into())
205 } else {
206 None
207 }
208 } else {
209 None
210 }
211 } else if self.base_path.is_file() {
212 self.count += 1;
213 Some(self.base_path.to_path_buf())
214 } else {
215 None
216 }
217 }
218 }
219
220 /// Iterator over logrotated files by returning a boxed reader
221 pub struct LogRotateFiles {
222 file_names: LogRotateFileNames,
223 }
224
225 impl Iterator for LogRotateFiles {
226 type Item = Box<dyn Read + Send>;
227
228 fn next(&mut self) -> Option<Self::Item> {
229 let filename = self.file_names.next()?;
230 let file = File::open(&filename).ok()?;
231
232 if filename.extension() == Some(std::ffi::OsStr::new("zst")) {
233 let encoder = zstd::stream::read::Decoder::new(file).ok()?;
234 return Some(Box::new(encoder));
235 }
236
237 Some(Box::new(file))
238 }
239 }