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
8pub struct FileRotate {
14 pub basename: OsString,
17 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 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 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 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}