use std::ffi::{OsStr, OsString};
use std::fs::{self, remove_file, rename, File, OpenOptions};
use std::io::{Error, Result, Seek, SeekFrom, Write};
use std::os::unix::fs::OpenOptionsExt;
use exacl::{setfacl, AclEntry, Perm};
pub struct FileRotate {
pub basename: OsString,
pub filesize: u64,
pub generations: u64,
pub users: Vec<String>,
pub groups: Vec<String>,
pub other: bool,
file: Option<File>,
offset: u64,
}
fn ignore_missing(e: Error) -> Result<()> {
if e.kind() == std::io::ErrorKind::NotFound {
Ok(())
} else {
Err(e)
}
}
impl FileRotate {
pub fn new<P: AsRef<OsStr>>(path: P) -> Self {
FileRotate {
basename: OsString::from(path.as_ref()),
filesize: 0,
generations: 0,
users: vec![],
groups: vec![],
other: false,
file: None,
offset: 0,
}
}
pub fn with_filesize(mut self, p: u64) -> Self {
self.filesize = p;
self
}
pub fn with_generations(mut self, p: u64) -> Self {
self.generations = p;
self
}
pub fn with_user(mut self, user: &str) -> Self {
self.users.push(user.into());
self
}
pub fn with_group(mut self, group: &str) -> Self {
self.groups.push(group.into());
self
}
pub fn with_other(mut self, other: bool) -> Self {
self.other = other;
self
}
pub fn rotate(&mut self) -> Result<()> {
log::info!("Rotating {}", self.basename.to_string_lossy());
if self.generations == 0 {
fs::remove_file(&self.basename).or_else(ignore_missing)?;
return Ok(());
}
for suffix in (0..self.generations).rev() {
let mut old = self.basename.clone();
match suffix {
0 => (),
_ => old.push(format!(".{suffix}")),
};
let mut new = self.basename.clone();
new.push(format!(".{}", suffix + 1));
if fs::metadata(&old).is_ok() {
fs::rename(old, new).or_else(ignore_missing)?;
}
}
self.file = None;
Ok(())
}
fn open(&mut self) -> Result<()> {
let mut acl = vec![
AclEntry::allow_user("", Perm::from_bits_truncate(6), None),
AclEntry::allow_group("", Perm::from_bits_truncate(4), None),
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
AclEntry::allow_other(
if self.other {
Perm::READ
} else {
Perm::empty()
},
None,
),
];
for user in &self.users {
acl.push(AclEntry::allow_user(user, Perm::READ, None));
}
for group in &self.groups {
acl.push(AclEntry::allow_group(group, Perm::READ, None));
}
if let Ok(mut f) = OpenOptions::new().append(true).open(&self.basename) {
setfacl(&[&self.basename], &acl, None).map_err(|e| Error::new(e.kind(), e))?;
self.offset = f.seek(SeekFrom::End(0))?;
self.file = Some(f);
} else {
let mut tmp = self.basename.clone();
tmp.push(".tmp");
remove_file(&tmp).or_else(|e| match e.kind() {
std::io::ErrorKind::NotFound => Ok(()),
_ => Err(e),
})?;
let f = OpenOptions::new()
.create_new(true)
.mode(0o600)
.append(true)
.open(&tmp)?;
setfacl(&[&tmp], &acl, None).map_err(|e| Error::new(e.kind(), e))?;
rename(&tmp, &self.basename)?;
self.offset = 0;
self.file = Some(f);
}
Ok(())
}
}
impl Write for FileRotate {
fn write(&mut self, buf: &[u8]) -> Result<usize> {
if self.file.is_none() {
self.open()?;
}
let mut f = self.file.as_ref().unwrap();
let sz = f.write(buf)?;
self.offset += sz as u64;
if self.offset > self.filesize && self.filesize != 0 && buf.last() == Some(&b'\n') {
f.sync_all()?;
self.rotate()?;
}
Ok(sz)
}
fn flush(&mut self) -> Result<()> {
match self.file.as_ref() {
Some(mut f) => f.flush(),
None => Ok(()),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use nix::unistd::mkdtemp;
use std::env::temp_dir;
#[test]
fn fresh_file() {
let td = mkdtemp(&temp_dir().join("laurel-test-XXXXXXXX")).expect("can't create temp dir");
let mut fr = FileRotate::new(td.join("logfile"));
fr.rotate().expect("rotate");
fr.write(b"asdf").expect("write");
fr.flush().expect("flush");
std::fs::remove_dir_all(td).expect("remove_dir_all");
}
#[test]
fn existing() {
let td = mkdtemp(&temp_dir().join("laurel-test-XXXXXXXX")).expect("can't create temp dir");
std::fs::write(&td.join("logfile"), "asdf").expect("setup");
let mut fr = FileRotate::new(&td.join("logfile")).with_generations(3);
fr.rotate().expect("rotate");
assert!(
td.join("logfile.1").exists(),
"after rotate, logfile.1 should exist"
);
assert!(
!td.join("logfile").exists(),
"after rotate, logfile should no longer exist"
);
fr.write(b"asdf").expect("write");
fr.flush().expect("flush");
assert!(
td.join("logfile").exists(),
"after rotate+write, logfile should exist"
);
std::fs::remove_dir_all(td).expect("remove_dir_all");
}
}