use std::fs;
use std::path::{Path, PathBuf};
use crate::error::{LoggerError, LoggerResult};
#[derive(Debug, PartialEq)]
pub enum RotationResult {
NotNeeded,
Completed,
Failed(LoggerError),
}
#[derive(Debug)]
pub struct SizeBasedRotation {
max_file_size: u64,
max_backup_files: u32,
}
impl SizeBasedRotation {
pub fn new(max_file_size: u64, max_backup_files: u32) -> Self {
Self {
max_file_size,
max_backup_files,
}
}
pub fn check_and_rotate(&self, log_file_path: &Path) -> RotationResult {
match self.needs_rotation(log_file_path) {
Ok(true) => self.perform_rotation(log_file_path),
Ok(false) => RotationResult::NotNeeded,
Err(error) => RotationResult::Failed(error),
}
}
fn needs_rotation(&self, log_file_path: &Path) -> LoggerResult<bool> {
match fs::metadata(log_file_path) {
Ok(metadata) => Ok(metadata.len() >= self.max_file_size),
Err(_) => {
Ok(false)
}
}
}
fn perform_rotation(&self, log_file_path: &Path) -> RotationResult {
let base_name = match log_file_path.file_stem() {
Some(name) => name.to_string_lossy(),
None => return RotationResult::Failed(LoggerError::RotationFailed {
current_file: log_file_path.display().to_string(),
backup_file: "unknown".to_string(),
reason: "Invalid file path".to_string(),
}),
};
let directory = log_file_path.parent().unwrap_or(Path::new("."));
if self.max_backup_files > 0 {
let oldest_backup = directory.join(format!("{}.{}.log", base_name, self.max_backup_files));
if oldest_backup.exists() {
if let Err(_) = fs::remove_file(&oldest_backup) {
return RotationResult::Failed(LoggerError::RotationFailed {
current_file: log_file_path.display().to_string(),
backup_file: oldest_backup.display().to_string(),
reason: "Failed to delete oldest backup".to_string(),
});
}
}
}
for i in (1..self.max_backup_files).rev() {
let current_backup = directory.join(format!("{}.{}.log", base_name, i));
let next_backup = directory.join(format!("{}.{}.log", base_name, i + 1));
if current_backup.exists() {
if let Err(_) = fs::rename(¤t_backup, &next_backup) {
return RotationResult::Failed(LoggerError::RotationFailed {
current_file: current_backup.display().to_string(),
backup_file: next_backup.display().to_string(),
reason: "Failed to shift backup file".to_string(),
});
}
}
}
if self.max_backup_files > 0 {
let first_backup = directory.join(format!("{}.1.log", base_name));
if let Err(_) = fs::rename(log_file_path, &first_backup) {
return RotationResult::Failed(LoggerError::RotationFailed {
current_file: log_file_path.display().to_string(),
backup_file: first_backup.display().to_string(),
reason: "Failed to move current log to backup".to_string(),
});
}
} else {
if let Err(_) = fs::remove_file(log_file_path) {
return RotationResult::Failed(LoggerError::RotationFailed {
current_file: log_file_path.display().to_string(),
backup_file: "none".to_string(),
reason: "Failed to delete current log (no backups configured)".to_string(),
});
}
}
RotationResult::Completed
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
use tempfile::tempdir;
#[test]
fn test_rotation_not_needed_for_small_file() {
let temp_dir = tempdir().unwrap();
let log_path = temp_dir.path().join("test.log");
let mut file = File::create(&log_path).unwrap();
file.write_all(&vec![b'x'; 100]).unwrap();
let rotation = SizeBasedRotation::new(1000, 3); let result = rotation.check_and_rotate(&log_path);
assert_eq!(result, RotationResult::NotNeeded);
}
#[test]
fn test_rotation_needed_for_large_file() {
let temp_dir = tempdir().unwrap();
let log_path = temp_dir.path().join("test.log");
let mut file = File::create(&log_path).unwrap();
file.write_all(&vec![b'x'; 2048]).unwrap();
drop(file);
let rotation = SizeBasedRotation::new(1000, 2); let result = rotation.check_and_rotate(&log_path);
assert_eq!(result, RotationResult::Completed);
assert!(!log_path.exists());
let backup_path = temp_dir.path().join("test.1.log");
assert!(backup_path.exists());
}
#[test]
fn test_no_rotation_for_nonexistent_file() {
let temp_dir = tempdir().unwrap();
let log_path = temp_dir.path().join("nonexistent.log");
let rotation = SizeBasedRotation::new(1000, 3);
let result = rotation.check_and_rotate(&log_path);
assert_eq!(result, RotationResult::NotNeeded);
}
}