1 use std
::path
::{Path, PathBuf}
;
2 use std
::fs
::{File, rename}
;
3 use std
::os
::unix
::io
::{FromRawFd, IntoRawFd}
;
6 use anyhow
::{bail, format_err, Error}
;
9 use proxmox
::tools
::fs
::{CreateOptions, make_tmp_file}
;
11 /// Used for rotating log files and iterating over them
12 pub struct LogRotate
{
16 /// User logs should be reowned to.
17 owner
: Option
<String
>,
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
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
>>(
31 Self::new_with_user(path
, compress
, Some(pbs_buildcfg
::BACKUP_USER_NAME
.to_owned()))
34 /// See [`new`]. Additionally this also takes a user which should by default be used to reown
36 pub fn new_with_user
<P
: AsRef
<Path
>>(
39 owner
: Option
<String
>,
41 if path
.as_ref().file_name().is_some() {
43 base_path
: path
.as_ref().to_path_buf(),
52 /// Returns an iterator over the logrotated file names that exist
53 pub fn file_names(&self) -> LogRotateFileNames
{
55 base_path
: self.base_path
.clone(),
57 compress
: self.compress
61 /// Returns an iterator over the logrotated file handles
62 pub fn files(&self) -> LogRotateFiles
{
64 file_names
: self.file_names(),
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
,
75 let _
= unistd
::unlink(&tmp_path
);
76 bail
!("creating zstd encoder failed - {}", err
);
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
);
85 if let Err(err
) = encoder
.finish() {
86 let _
= unistd
::unlink(&tmp_path
);
87 bail
!("zstd finish failed for file {:?} - {}", target_path
, err
);
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
);
95 if let Err(err
) = unistd
::unlink(source_path
) {
96 bail
!("unlink failed for file {:?} - {}", source_path
, err
);
102 /// Rotates the files up to 'max_files'
103 /// if the 'compress' option was given it will compress the newest file
106 /// foo.2.zst => foo.3.zst
107 /// foo.1 => foo.2.zst
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
114 let count
= filenames
.len() + 1;
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");
122 filenames
.push(PathBuf
::from(next_filename
));
124 for i
in (0..count
-1).rev() {
126 && filenames
[i
].extension() != Some(std
::ffi
::OsStr
::new("zst"))
127 && filenames
[i
+1].extension() == Some(std
::ffi
::OsStr
::new("zst"))
129 Self::compress(&filenames
[i
], &filenames
[i
+1], &options
)?
;
131 rename(&filenames
[i
], &filenames
[i
+1])?
;
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
);
149 options
: Option
<CreateOptions
>,
150 max_files
: Option
<usize>
151 ) -> Result
<bool
, Error
> {
153 let options
= match options
{
154 Some(options
) => options
,
155 None
=> match self.owner
.as_deref() {
157 let user
= crate::sys
::query_user(owner
)?
159 format_err
!("failed to lookup owning user '{}' for logs", owner
)
161 CreateOptions
::new().owner(user
.uid
).group(user
.gid
)
163 None
=> CreateOptions
::new(),
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
),
173 if metadata
.len() > max_size
{
174 self.do_rotate(options
, max_files
)?
;
182 /// Iterator over logrotated file names
183 pub struct LogRotateFileNames
{
189 impl Iterator
for LogRotateFileNames
{
192 fn next(&mut self) -> Option
<Self::Item
> {
194 let mut path
: std
::ffi
::OsString
= self.base_path
.clone().into();
196 path
.push(format
!(".{}", self.count
));
199 if Path
::new(&path
).is_file() {
201 } else if self.compress
{
203 if Path
::new(&path
).is_file() {
211 } else if self.base_path
.is_file() {
213 Some(self.base_path
.to_path_buf())
220 /// Iterator over logrotated files by returning a boxed reader
221 pub struct LogRotateFiles
{
222 file_names
: LogRotateFileNames
,
225 impl Iterator
for LogRotateFiles
{
226 type Item
= Box
<dyn Read
+ Send
>;
228 fn next(&mut self) -> Option
<Self::Item
> {
229 let filename
= self.file_names
.next()?
;
230 let file
= File
::open(&filename
).ok()?
;
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
));