use crate::error::{LoglyError, Result};
use chrono::{DateTime, Utc};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub enum RotationPolicy {
Size(u64),
Time(String),
Both(u64, String),
}
pub struct RotationManager {
base_path: PathBuf,
policy: RotationPolicy,
retention: Option<usize>,
current_size: u64,
last_rotation: DateTime<Utc>,
}
impl RotationManager {
pub fn new(base_path: PathBuf, policy: RotationPolicy, retention: Option<usize>) -> Self {
Self {
base_path,
policy,
retention,
current_size: 0,
last_rotation: Utc::now(),
}
}
pub fn should_rotate(&mut self, additional_size: u64) -> bool {
match &self.policy {
RotationPolicy::Size(max_size) => self.current_size + additional_size >= *max_size,
RotationPolicy::Time(interval) => self.should_rotate_by_time(interval),
RotationPolicy::Both(max_size, interval) => {
(self.current_size + additional_size >= *max_size)
|| self.should_rotate_by_time(interval)
}
}
}
fn should_rotate_by_time(&self, interval: &str) -> bool {
let now = Utc::now();
let duration = now.signed_duration_since(self.last_rotation);
match interval.to_lowercase().as_str() {
"hourly" => duration.num_hours() >= 1,
"daily" => duration.num_days() >= 1,
"weekly" => duration.num_weeks() >= 1,
"monthly" => duration.num_days() >= 30,
"yearly" => duration.num_days() >= 365,
_ => false,
}
}
pub fn rotate(&mut self) -> Result<PathBuf> {
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
let extension = self
.base_path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("log");
let stem = self
.base_path
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| LoglyError::InvalidConfig("Invalid file path".to_string()))?;
let parent = self
.base_path
.parent()
.ok_or_else(|| LoglyError::InvalidConfig("Invalid file path".to_string()))?;
let rotated_path = parent.join(format!("{}_{}.{}", stem, timestamp, extension));
if self.base_path.exists() {
fs::rename(&self.base_path, &rotated_path)?;
}
self.current_size = 0;
self.last_rotation = Utc::now();
if let Some(retention) = self.retention {
self.apply_retention(parent, stem, extension, retention)?;
}
Ok(rotated_path)
}
fn apply_retention(
&self,
dir: &Path,
stem: &str,
extension: &str,
max_files: usize,
) -> Result<()> {
let mut log_files: Vec<_> = fs::read_dir(dir)?
.filter_map(|entry| entry.ok())
.filter(|entry| {
if let Some(name) = entry.file_name().to_str() {
name.starts_with(stem) && name.ends_with(extension)
} else {
false
}
})
.collect();
log_files.sort_by_key(|entry| {
entry
.metadata()
.and_then(|m| m.modified())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
});
if log_files.len() > max_files {
for entry in log_files.iter().take(log_files.len() - max_files) {
fs::remove_file(entry.path())?;
}
}
Ok(())
}
pub fn update_size(&mut self, size: u64) {
self.current_size += size;
}
pub fn current_size(&self) -> u64 {
self.current_size
}
}