]> git.proxmox.com Git - proxmox-backup.git/blame - src/tools/logrotate.rs
d/control: add ',' after qrencode dependency
[proxmox-backup.git] / src / tools / logrotate.rs
CommitLineData
8074d2b0
DC
1use std::path::{Path, PathBuf};
2use std::fs::{File, rename};
3use std::os::unix::io::FromRawFd;
4use std::io::Read;
5
6use anyhow::{bail, Error};
7use nix::unistd;
8
9use proxmox::tools::fs::{CreateOptions, make_tmp_file, replace_file};
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
49 /// Rotates the files up to 'max_files'
50 /// if the 'compress' option was given it will compress the newest file
51 ///
52 /// e.g. rotates
53 /// foo.2.zst => foo.3.zst
54 /// foo.1.zst => foo.2.zst
55 /// foo => foo.1.zst
56 /// => foo
57 pub fn rotate(&mut self, options: CreateOptions, max_files: Option<usize>) -> Result<(), Error> {
58 let mut filenames: Vec<PathBuf> = self.file_names().collect();
59 if filenames.is_empty() {
60 return Ok(()); // no file means nothing to rotate
61 }
62
63 let mut next_filename = self.base_path.clone().canonicalize()?.into_os_string();
64
65 if self.compress {
66 next_filename.push(format!(".{}.zst", filenames.len()));
67 } else {
68 next_filename.push(format!(".{}", filenames.len()));
69 }
70
71 filenames.push(PathBuf::from(next_filename));
72 let count = filenames.len();
73
74 // rotate all but the first, that we maybe have to compress
75 for i in (1..count-1).rev() {
76 rename(&filenames[i], &filenames[i+1])?;
77 }
78
79 if self.compress {
80 let mut source = File::open(&filenames[0])?;
81 let (fd, tmp_path) = make_tmp_file(&filenames[1], options.clone())?;
82 let target = unsafe { File::from_raw_fd(fd) };
83 let mut encoder = match zstd::stream::write::Encoder::new(target, 0) {
84 Ok(encoder) => encoder,
85 Err(err) => {
86 let _ = unistd::unlink(&tmp_path);
87 bail!("creating zstd encoder failed - {}", err);
88 }
89 };
90
91 if let Err(err) = std::io::copy(&mut source, &mut encoder) {
92 let _ = unistd::unlink(&tmp_path);
93 bail!("zstd encoding failed for file {:?} - {}", &filenames[1], err);
94 }
95
96 if let Err(err) = encoder.finish() {
97 let _ = unistd::unlink(&tmp_path);
98 bail!("zstd finish failed for file {:?} - {}", &filenames[1], err);
99 }
100
101 if let Err(err) = rename(&tmp_path, &filenames[1]) {
102 let _ = unistd::unlink(&tmp_path);
103 bail!("rename failed for file {:?} - {}", &filenames[1], err);
104 }
105
106 unistd::unlink(&filenames[0])?;
107 } else {
108 rename(&filenames[0], &filenames[1])?;
109 }
110
111 // create empty original file
112 replace_file(&filenames[0], b"", options)?;
113
114 if let Some(max_files) = max_files {
115 // delete all files > max_files
116 for file in filenames.iter().skip(max_files) {
117 if let Err(err) = unistd::unlink(file) {
118 eprintln!("could not remove {:?}: {}", &file, err);
119 }
120 }
121 }
122
123 Ok(())
124 }
125}
126
127/// Iterator over logrotated file names
128pub struct LogRotateFileNames {
129 base_path: PathBuf,
130 count: usize,
131 compress: bool,
132}
133
134impl Iterator for LogRotateFileNames {
135 type Item = PathBuf;
136
137 fn next(&mut self) -> Option<Self::Item> {
138 if self.count > 0 {
139 let mut path: std::ffi::OsString = self.base_path.clone().into();
140
141 path.push(format!(".{}", self.count));
142 self.count += 1;
143
144 if Path::new(&path).is_file() {
145 Some(path.into())
146 } else if self.compress {
147 path.push(".zst");
148 if Path::new(&path).is_file() {
149 Some(path.into())
150 } else {
151 None
152 }
153 } else {
154 None
155 }
156 } else if self.base_path.is_file() {
157 self.count += 1;
158 Some(self.base_path.to_path_buf())
159 } else {
160 None
161 }
162 }
163}
164
165/// Iterator over logrotated files by returning a boxed reader
166pub struct LogRotateFiles {
167 file_names: LogRotateFileNames,
168}
169
170impl Iterator for LogRotateFiles {
171 type Item = Box<dyn Read + Send>;
172
173 fn next(&mut self) -> Option<Self::Item> {
174 let filename = self.file_names.next()?;
175 let file = File::open(&filename).ok()?;
176
177 if filename.extension().unwrap_or(std::ffi::OsStr::new("")) == "zst" {
178 let encoder = zstd::stream::read::Decoder::new(file).ok()?;
179 return Some(Box::new(encoder));
180 }
181
182 Some(Box::new(file))
183 }
184}