use std::fs::{self, File, OpenOptions};
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
fn compress_gzip(source: &Path) -> io::Result<()> {
let mut input = Vec::new();
File::open(source)?.read_to_end(&mut input)?;
if input.is_empty() {
return Ok(());
}
let compressed = deflate(&input);
let gz_path = source.with_extension(
format!("{}.gz", source.extension().unwrap_or_default().to_string_lossy())
.trim_start_matches('.'),
);
let mut out = File::create(&gz_path)?;
out.write_all(&[
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, ])?;
out.write_all(&compressed)?;
let crc = crc32(&input);
let len = (input.len() as u32).to_le_bytes();
out.write_all(&crc.to_le_bytes())?;
out.write_all(&len)?;
out.flush()?;
drop(out);
fs::remove_file(source)?;
Ok(())
}
fn deflate(data: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
let mut pos = 0;
while pos < data.len() {
let is_final = pos + 65535 >= data.len();
let block_size = std::cmp::min(65535, data.len() - pos);
let chunk = &data[pos..pos + block_size];
let bfinal_bit = if is_final { 1 } else { 0 };
out.push(bfinal_bit);
let len = block_size as u16;
out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(&(!len).to_le_bytes());
out.extend_from_slice(chunk);
pos += block_size;
}
out
}
fn crc32(data: &[u8]) -> u32 {
let table = crc32_table();
let mut crc = 0xffffffffu32;
for &byte in data {
crc = table[((crc ^ byte as u32) & 0xff) as usize] ^ (crc >> 8);
}
crc ^ 0xffffffff
}
fn crc32_table() -> [u32; 256] {
let mut table = [0u32; 256];
for i in 0..256 {
let mut crc = i as u32;
for _ in 0..8 {
if crc & 1 != 0 {
crc = 0xedb88320 ^ (crc >> 1);
} else {
crc >>= 1;
}
}
table[i] = crc;
}
table
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RotationStrategy {
SizeBased(u64),
Daily,
Combined {
size_limit: u64
},
}
impl Default for RotationStrategy {
fn default() -> Self {
RotationStrategy::SizeBased(10 * 1024 * 1024) }
}
#[derive(Debug, Clone)]
pub struct RotatorConfig {
pub strategy: RotationStrategy,
pub max_files: usize,
pub compress_old_files: bool,
}
impl Default for RotatorConfig {
fn default() -> Self {
RotatorConfig {
strategy: RotationStrategy::SizeBased(10 * 1024 * 1024),
max_files: 10,
compress_old_files: false,
}
}
}
pub struct Rotator {
base_path: PathBuf,
current_file: Mutex<Option<File>>,
config: RotatorConfig,
file_counter: Mutex<usize>,
}
impl Rotator {
pub fn new(base_path: &str, config: RotatorConfig) -> Self {
Rotator {
base_path: PathBuf::from(base_path),
current_file: Mutex::new(None),
config,
file_counter: Mutex::new(0),
}
}
pub fn base_path(&self) -> &Path {
&self.base_path
}
pub fn init_or_rotate(&self) -> io::Result<()> {
let mut current_file = self.current_file.lock().unwrap();
if current_file.is_none() {
*current_file = Some(self.create_new_file()?);
} else {
if self.needs_rotation()? {
self.rotate()?;
}
}
Ok(())
}
pub fn needs_rotation(&self) -> io::Result<bool> {
let current_file = self.current_file.lock().unwrap();
if let Some(file) = current_file.as_ref() {
match self.config.strategy {
RotationStrategy::SizeBased(limit) => {
let metadata = file.metadata()?;
Ok(metadata.len() >= limit)
}
RotationStrategy::Daily => {
Ok(self.is_new_day())
}
RotationStrategy::Combined { size_limit } => {
let metadata = file.metadata()?;
Ok(metadata.len() >= size_limit || self.is_new_day())
}
}
} else {
Ok(false)
}
}
fn is_new_day(&self) -> bool {
let current_filename = self.get_current_filename();
let expected_filename = self.generate_filename(0);
current_filename != expected_filename
}
pub fn rotate(&self) -> io::Result<()> {
let mut current_file = self.current_file.lock().unwrap();
if let Some(mut file) = current_file.take() {
file.flush()?;
let old_path = self.get_current_file_path();
let new_path = self.get_rotated_file_path();
drop(file);
fs::rename(&old_path, &new_path)?;
if self.config.compress_old_files {
let _ = compress_gzip(&new_path);
}
self.cleanup_old_files()?;
*current_file = Some(self.create_new_file()?);
}
Ok(())
}
fn create_new_file(&self) -> io::Result<File> {
let path = self.get_current_file_path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
OpenOptions::new()
.create(true)
.write(true)
.append(true)
.open(&path)
}
fn get_current_file_path(&self) -> PathBuf {
let filename = self.get_current_filename();
self.base_path.with_file_name(filename)
}
fn get_rotated_file_path(&self) -> PathBuf {
let counter = {
let mut c = self.file_counter.lock().unwrap();
*c += 1;
*c
};
let filename = self.generate_filename(counter);
self.base_path.with_file_name(filename)
}
fn generate_filename(&self, counter: usize) -> String {
let base = self.base_path.file_name().unwrap().to_string_lossy();
let ext = self.base_path.extension().unwrap_or_default().to_string_lossy();
if ext.is_empty() {
if counter > 0 {
format!("{}.{}", base, counter)
} else {
base.to_string()
}
} else {
let stem = base.strip_suffix(&format!(".{}", ext)).unwrap_or(&base);
if counter > 0 {
format!("{}.{}.{}", stem, counter, ext)
} else {
base.to_string()
}
}
}
fn get_current_filename(&self) -> String {
self.generate_filename(0)
}
fn cleanup_old_files(&self) -> io::Result<()> {
if self.config.max_files == 0 {
return Ok(());
}
let parent = match self.base_path.parent() {
Some(p) => p,
None => return Ok(()),
};
let file_stem = self.base_path.file_stem().unwrap().to_string_lossy();
let file_ext = self.base_path.extension().unwrap_or_default().to_string_lossy();
let mut files: Vec<(PathBuf, u64)> = fs::read_dir(parent)?
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
let file_name = path.file_name()?.to_string_lossy();
if file_ext.is_empty() {
if let Some(num_str) = file_name.strip_prefix(&format!("{}.", file_stem)) {
if let Ok(num) = num_str.parse::<u64>() {
return Some((path, num));
}
}
} else {
let pattern = format!("{}.", file_stem);
if let Some(rest) = file_name.strip_prefix(&pattern) {
if let Some(dot_idx) = rest.rfind('.') {
let (num_str, ext) = rest.split_at(dot_idx);
if ext == &format!(".{}", file_ext) {
if let Ok(num) = num_str.parse::<u64>() {
return Some((path, num));
}
}
}
}
}
None
})
.collect();
files.sort_by(|a, b| b.1.cmp(&a.1));
for (path, _) in files.into_iter().skip(self.config.max_files) {
fs::remove_file(path)?;
}
Ok(())
}
pub fn write(&self, data: &[u8]) -> io::Result<()> {
self.init_or_rotate()?;
let mut current_file = self.current_file.lock().unwrap();
if let Some(file) = current_file.as_mut() {
file.write_all(data)?;
file.flush()?;
}
Ok(())
}
pub fn writeln(&self, line: &str) -> io::Result<()> {
self.write(line.as_bytes())?;
self.write(b"\n")
}
pub fn config(&self) -> &RotatorConfig {
&self.config
}
}