laurel/
rotate.rs

1use std::ffi::{OsStr, OsString};
2use std::fs::{self, remove_file, rename, File, OpenOptions};
3use std::io::{Error, Result, Seek, SeekFrom, Write};
4use std::os::unix::fs::OpenOptionsExt;
5
6use exacl::{setfacl, AclEntry, Perm};
7
8/// A rotating (log) file writer
9///
10/// [`FileRotate`] rotates the file after `filesize` bytes have been
11/// written to the main file. Up to `num_files` generations of backup
12/// files are kept around.
13pub struct FileRotate {
14    /// The name for the main file. For backup generations, `.1`,
15    /// `.2`, `.3` etc. are appended to this file name.
16    pub basename: OsString,
17    /// When a [`write`] operation causes the main file to reach this
18    /// size, a [`FileRotate::rotate`] operation is triggered.
19    pub filesize: u64,
20    pub generations: u64,
21    pub users: Vec<String>,
22    pub groups: Vec<String>,
23    pub other: bool,
24    file: Option<File>,
25    offset: u64,
26}
27
28fn ignore_missing(e: Error) -> Result<()> {
29    if e.kind() == std::io::ErrorKind::NotFound {
30        Ok(())
31    } else {
32        Err(e)
33    }
34}
35
36impl FileRotate {
37    /// Creates a new [`FileRotate`] instance. This does not involve
38    /// any I/O operations; the main file is only created when calling
39    /// [`write`].
40    pub fn new<P: AsRef<OsStr>>(path: P) -> Self {
41        FileRotate {
42            basename: OsString::from(path.as_ref()),
43            filesize: 0,
44            generations: 0,
45            users: vec![],
46            groups: vec![],
47            other: false,
48            file: None,
49            offset: 0,
50        }
51    }
52
53    pub fn with_filesize(mut self, p: u64) -> Self {
54        self.filesize = p;
55        self
56    }
57    pub fn with_generations(mut self, p: u64) -> Self {
58        self.generations = p;
59        self
60    }
61    pub fn with_user(mut self, user: &str) -> Self {
62        self.users.push(user.into());
63        self
64    }
65    pub fn with_group(mut self, group: &str) -> Self {
66        self.groups.push(group.into());
67        self
68    }
69    pub fn with_other(mut self, other: bool) -> Self {
70        self.other = other;
71        self
72    }
73
74    /// Closes the main file and performs a backup file rotation
75    pub fn rotate(&mut self) -> Result<()> {
76        log::info!("Rotating {}", self.basename.to_string_lossy());
77        if self.generations == 0 {
78            fs::remove_file(&self.basename).or_else(ignore_missing)?;
79            return Ok(());
80        }
81        for suffix in (0..self.generations).rev() {
82            let mut old = self.basename.clone();
83            match suffix {
84                0 => (),
85                _ => old.push(format!(".{suffix}")),
86            };
87            let mut new = self.basename.clone();
88            new.push(format!(".{}", suffix + 1));
89            if fs::metadata(&old).is_ok() {
90                fs::rename(old, new).or_else(ignore_missing)?;
91            }
92        }
93        self.file = None;
94        Ok(())
95    }
96
97    /// Opens main file, re-using existing file if prersent.
98    ///
99    /// If the file does not exist, a new temporary file is crerated,
100    /// permissions are adjusted, and it is renamed to the final
101    /// destination.
102    fn open(&mut self) -> Result<()> {
103        let mut acl = vec![
104            AclEntry::allow_user("", Perm::from_bits_truncate(6), None),
105            AclEntry::allow_group("", Perm::from_bits_truncate(4), None),
106            #[cfg(any(target_os = "linux", target_os = "freebsd"))]
107            AclEntry::allow_other(
108                if self.other {
109                    Perm::READ
110                } else {
111                    Perm::empty()
112                },
113                None,
114            ),
115        ];
116        for user in &self.users {
117            acl.push(AclEntry::allow_user(user, Perm::READ, None));
118        }
119        for group in &self.groups {
120            acl.push(AclEntry::allow_group(group, Perm::READ, None));
121        }
122
123        if let Ok(mut f) = OpenOptions::new().append(true).open(&self.basename) {
124            setfacl(&[&self.basename], &acl, None).map_err(|e| Error::new(e.kind(), e))?;
125
126            self.offset = f.seek(SeekFrom::End(0))?;
127            self.file = Some(f);
128        } else {
129            let mut tmp = self.basename.clone();
130            tmp.push(".tmp");
131
132            remove_file(&tmp).or_else(|e| match e.kind() {
133                std::io::ErrorKind::NotFound => Ok(()),
134                _ => Err(e),
135            })?;
136
137            let f = OpenOptions::new()
138                .create_new(true)
139                .mode(0o600)
140                .append(true)
141                .open(&tmp)?;
142
143            setfacl(&[&tmp], &acl, None).map_err(|e| Error::new(e.kind(), e))?;
144
145            rename(&tmp, &self.basename)?;
146
147            self.offset = 0;
148            self.file = Some(f);
149        }
150        Ok(())
151    }
152}
153
154impl Write for FileRotate {
155    fn write(&mut self, buf: &[u8]) -> Result<usize> {
156        if self.file.is_none() {
157            self.open()?;
158        }
159        let mut f = self.file.as_ref().unwrap();
160        let sz = f.write(buf)?;
161        self.offset += sz as u64;
162        if self.offset > self.filesize && self.filesize != 0 && buf.last() == Some(&b'\n') {
163            f.sync_all()?;
164            self.rotate()?;
165        }
166        Ok(sz)
167    }
168    fn flush(&mut self) -> Result<()> {
169        match self.file.as_ref() {
170            Some(mut f) => f.flush(),
171            None => Ok(()),
172        }
173    }
174}
175
176#[cfg(test)]
177mod test {
178    use super::*;
179    use nix::unistd::mkdtemp;
180    use std::env::temp_dir;
181    #[test]
182    fn fresh_file() {
183        let td = mkdtemp(&temp_dir().join("laurel-test-XXXXXXXX")).expect("can't create temp dir");
184        let mut fr = FileRotate::new(td.join("logfile"));
185        fr.rotate().expect("rotate");
186        fr.write(b"asdf").expect("write");
187        fr.flush().expect("flush");
188        std::fs::remove_dir_all(td).expect("remove_dir_all");
189    }
190
191    #[test]
192    fn existing() {
193        let td = mkdtemp(&temp_dir().join("laurel-test-XXXXXXXX")).expect("can't create temp dir");
194        std::fs::write(td.join("logfile"), "asdf").expect("setup");
195        let mut fr = FileRotate::new(td.join("logfile")).with_generations(3);
196        fr.rotate().expect("rotate");
197        assert!(
198            td.join("logfile.1").exists(),
199            "after rotate, logfile.1 should exist"
200        );
201        assert!(
202            !td.join("logfile").exists(),
203            "after rotate, logfile should no longer exist"
204        );
205        fr.write(b"asdf").expect("write");
206        fr.flush().expect("flush");
207        assert!(
208            td.join("logfile").exists(),
209            "after rotate+write, logfile should exist"
210        );
211        std::fs::remove_dir_all(td).expect("remove_dir_all");
212    }
213}