use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy)]
pub struct RotationPolicy {
pub max_size_bytes: u64,
pub max_files: usize,
}
impl Default for RotationPolicy {
fn default() -> Self {
Self {
max_size_bytes: 100 * 1024 * 1024,
max_files: 3,
}
}
}
pub fn maybe_rotate(path: &Path, policy: RotationPolicy) -> std::io::Result<bool> {
let size = match fs::metadata(path) {
Ok(m) => m.len(),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(false),
Err(e) => return Err(e),
};
if size <= policy.max_size_bytes {
return Ok(false);
}
rotate_chain(path, policy.max_files)?;
Ok(true)
}
fn rotated_path(base: &Path, n: usize) -> PathBuf {
let mut s = base.as_os_str().to_owned();
s.push(format!(".{n}"));
PathBuf::from(s)
}
fn rotate_chain(base: &Path, max_files: usize) -> std::io::Result<()> {
let oldest = rotated_path(base, max_files);
if oldest.exists() {
fs::remove_file(&oldest)?;
}
for n in (2..=max_files).rev() {
let from = rotated_path(base, n - 1);
let to = rotated_path(base, n);
if from.exists() {
fs::rename(&from, &to)?;
}
}
fs::rename(base, rotated_path(base, 1))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn temp_dir(tag: &str) -> PathBuf {
let dir =
std::env::temp_dir().join(format!("anvil-runner-rotate-{}-{tag}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn write_n_bytes(p: &Path, n: usize) {
let mut f = fs::File::create(p).unwrap();
f.write_all(&vec![b'x'; n]).unwrap();
}
#[test]
fn no_rotate_when_under_threshold() {
let dir = temp_dir("under");
let log = dir.join("runner.log");
write_n_bytes(&log, 100);
let rotated = maybe_rotate(
&log,
RotationPolicy {
max_size_bytes: 1000,
max_files: 3,
},
)
.unwrap();
assert!(!rotated);
assert_eq!(fs::metadata(&log).unwrap().len(), 100);
}
#[test]
fn rotates_when_over_threshold() {
let dir = temp_dir("over");
let log = dir.join("runner.log");
write_n_bytes(&log, 2000);
let rotated = maybe_rotate(
&log,
RotationPolicy {
max_size_bytes: 1000,
max_files: 3,
},
)
.unwrap();
assert!(rotated);
assert!(!log.exists(), "original should be moved aside");
assert!(
rotated_path(&log, 1).exists(),
".1 should now hold prior contents"
);
}
#[test]
fn rotation_chain_shifts_and_drops_oldest() {
let dir = temp_dir("chain");
let log = dir.join("runner.log");
fs::write(rotated_path(&log, 1), b"one").unwrap();
fs::write(rotated_path(&log, 2), b"two").unwrap();
fs::write(rotated_path(&log, 3), b"three").unwrap();
write_n_bytes(&log, 5000);
maybe_rotate(
&log,
RotationPolicy {
max_size_bytes: 1000,
max_files: 3,
},
)
.unwrap();
assert_eq!(fs::read_to_string(rotated_path(&log, 3)).unwrap(), "two");
assert_eq!(fs::read_to_string(rotated_path(&log, 2)).unwrap(), "one");
assert_eq!(fs::metadata(rotated_path(&log, 1)).unwrap().len(), 5000);
}
#[test]
fn no_rotate_when_log_absent() {
let dir = temp_dir("absent");
let log = dir.join("never-existed.log");
let rotated = maybe_rotate(&log, RotationPolicy::default()).unwrap();
assert!(!rotated);
}
}