]>
Commit | Line | Data |
---|---|---|
8074d2b0 DC |
1 | use std::path::{Path, PathBuf}; |
2 | use std::fs::{File, rename}; | |
b6570abe | 3 | use std::os::unix::io::{FromRawFd, IntoRawFd}; |
8074d2b0 DC |
4 | use std::io::Read; |
5 | ||
6 | use anyhow::{bail, Error}; | |
7 | use nix::unistd; | |
8 | ||
e6ca9c32 | 9 | use proxmox::tools::fs::{CreateOptions, make_tmp_file}; |
8074d2b0 DC |
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 | ||
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 |
e062ebbc FG |
107 | && filenames[i+0].extension() != Some(std::ffi::OsStr::new("zst")) |
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 | |
158 | pub struct LogRotateFileNames { | |
159 | base_path: PathBuf, | |
160 | count: usize, | |
161 | compress: bool, | |
162 | } | |
163 | ||
164 | impl 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 | |
196 | pub struct LogRotateFiles { | |
197 | file_names: LogRotateFileNames, | |
198 | } | |
199 | ||
200 | impl 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 | } |