use std::fs;
use std::io;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct RotationConfig {
pub max_size: u64,
pub max_files: usize,
}
impl RotationConfig {
pub fn new(max_size: u64, max_files: usize) -> Self {
Self {
max_size,
max_files,
}
}
pub fn megabytes(mb: u64, max_files: usize) -> Self {
Self::new(mb * 1_000_000, max_files)
}
}
impl Default for RotationConfig {
fn default() -> Self {
Self::new(10_000_000, 5)
}
}
pub(crate) struct Rotator {
base_path: PathBuf,
config: RotationConfig,
}
impl Rotator {
pub fn new(path: impl Into<PathBuf>, config: RotationConfig) -> Self {
Self {
base_path: path.into(),
config,
}
}
pub fn rotate_if_needed(&self) -> io::Result<bool> {
let metadata = match fs::metadata(&self.base_path) {
Ok(m) => m,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false),
Err(e) => return Err(e),
};
if metadata.len() >= self.config.max_size {
self.rotate()?;
Ok(true)
} else {
Ok(false)
}
}
fn rotate(&self) -> io::Result<()> {
let oldest = self.rotated_path(self.config.max_files);
if oldest.exists() {
fs::remove_file(&oldest)?;
}
for i in (1..self.config.max_files).rev() {
let from = self.rotated_path(i);
let to = self.rotated_path(i + 1);
if from.exists() {
fs::rename(&from, &to)?;
}
}
let first_rotated = self.rotated_path(1);
if self.base_path.exists() {
fs::rename(&self.base_path, &first_rotated)?;
}
Ok(())
}
fn rotated_path(&self, index: usize) -> PathBuf {
let mut path = self.base_path.as_os_str().to_owned();
path.push(format!(".{}", index));
PathBuf::from(path)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn test_rotation_config_defaults() {
let config = RotationConfig::default();
assert_eq!(config.max_size, 10_000_000);
assert_eq!(config.max_files, 5);
}
#[test]
fn test_rotation_config_megabytes() {
let config = RotationConfig::megabytes(5, 3);
assert_eq!(config.max_size, 5_000_000);
assert_eq!(config.max_files, 3);
}
#[test]
fn test_rotate_creates_backup() {
let dir = std::env::temp_dir().join("prologger_test_rotation");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let log_path = dir.join("test.log");
{
let mut f = fs::File::create(&log_path).unwrap();
f.write_all(&[b'x'; 100]).unwrap();
}
let rotator = Rotator::new(&log_path, RotationConfig::new(50, 3));
assert!(rotator.rotate_if_needed().unwrap());
assert!(!log_path.exists());
assert!(dir.join("test.log.1").exists());
let _ = fs::remove_dir_all(&dir);
}
#[test]
fn test_no_rotation_under_limit() {
let dir = std::env::temp_dir().join("prologger_test_no_rotation");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
let log_path = dir.join("test.log");
{
let mut f = fs::File::create(&log_path).unwrap();
f.write_all(&[b'x'; 10]).unwrap();
}
let rotator = Rotator::new(&log_path, RotationConfig::new(100, 3));
assert!(!rotator.rotate_if_needed().unwrap());
assert!(log_path.exists());
let _ = fs::remove_dir_all(&dir);
}
}