use std::fs::{File, OpenOptions};
use std::io::{self, Write};
use std::option::Option;
use std::path::Path;
pub struct RotatingFileHandler {
base_path: String,
max_bytes: u64,
backup_count: usize,
current_size: u64,
file: File,
header: Option<Vec<u8>>,
}
impl RotatingFileHandler {
pub fn new(
base_path: &str,
max_bytes: u64,
backup_count: usize,
header: Option<Vec<u8>>,
) -> io::Result<Self> {
let mut file = OpenOptions::new()
.append(true)
.create(true)
.open(base_path)?;
if let Some(ref header) = header {
if header.len() as u64 > max_bytes {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Header size exceeds maximum file size",
));
}
file.write_all(header)?;
}
let current_size = file.metadata()?.len();
Ok(Self {
base_path: base_path.to_string(),
max_bytes,
backup_count,
current_size,
file,
header,
})
}
fn rotate(&mut self) -> io::Result<()> {
self.file.flush()?; for i in (1..self.backup_count).rev() {
let src = format!("{}.{}", self.base_path, i - 1);
let dst = format!("{}.{}", self.base_path, i);
if Path::new(&src).exists() {
std::fs::rename(src, dst)?; }
}
std::fs::rename(&self.base_path, format!("{}.0", self.base_path))?; self.file = OpenOptions::new()
.append(true)
.create(true)
.open(&self.base_path)?; if let Some(ref header) = self.header {
self.file.write_all(header)?; }
self.current_size = 0; Ok(())
}
pub fn emit(&mut self, bytes: &[u8]) -> io::Result<()> {
if self.current_size + bytes.len() as u64 > self.max_bytes {
self.rotate()?; }
self.file.write_all(bytes)?; self.current_size += bytes.len() as u64; Ok(())
}
}
impl Write for RotatingFileHandler {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.emit(buf)?;
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.file.flush()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write;
#[test]
fn test_rotation_on_max_file_size() {
let mut handler = RotatingFileHandler::new("test_case_1_log.txt", 10, 3, None).unwrap();
handler.emit(b"12345").unwrap();
handler.emit(b"67890").unwrap();
assert!(Path::new("test_case_1_log.txt").exists());
assert!(!Path::new("test_case_1_log.txt.0").exists());
handler.emit(b"abcde").unwrap();
handler.emit(b"fghij").unwrap();
assert!(Path::new("test_case_1_log.txt").exists());
assert!(Path::new("test_case_1_log.txt.0").exists());
assert!(!Path::new("test_case_1_log.txt.1").exists());
let content = fs::read_to_string("test_case_1_log.txt").unwrap();
assert_eq!(content, "abcdefghij");
let content = fs::read_to_string("test_case_1_log.txt.0").unwrap();
assert_eq!(content, "1234567890");
let _ = fs::remove_file("test_case_1_log.txt");
for i in 0..1 {
let _ = fs::remove_file(format!("test_case_1_log.txt.{}", i));
}
}
#[test]
fn test_rotation_on_max_count() {
let mut handler = RotatingFileHandler::new("test_case_2_log.txt", 10, 2, None).unwrap();
handler.emit(b"1234567890").unwrap();
handler.emit(b"abcdefghij").unwrap(); handler.emit(b"klmnopqrst").unwrap(); handler.emit(b"uvwxyzabcd").unwrap();
assert!(Path::new("test_case_2_log.txt").exists());
assert!(Path::new("test_case_2_log.txt.0").exists());
assert!(Path::new("test_case_2_log.txt.1").exists());
assert!(!Path::new("test_case_2_log.txt.2").exists());
let content = fs::read_to_string("test_case_2_log.txt").unwrap();
assert_eq!(content, "uvwxyzabcd");
let content = fs::read_to_string("test_case_2_log.txt.0").unwrap();
assert_eq!(content, "klmnopqrst");
let content = fs::read_to_string("test_case_2_log.txt.1").unwrap();
assert_eq!(content, "abcdefghij");
let _ = fs::remove_file("test_case_2_log.txt");
for i in 0..2 {
let _ = fs::remove_file(format!("test_case_2_log.txt.{}", i));
}
}
#[test]
fn test_emit() {
let mut handler = RotatingFileHandler::new("test_case_3_log.txt", 50, 1, None).unwrap();
handler.emit(b"Hello, world!").unwrap();
handler.emit(b" More data.").unwrap();
let content = fs::read_to_string("test_case_3_log.txt").unwrap();
assert_eq!(content, "Hello, world! More data.");
let _ = fs::remove_file("test_case_3_log.txt");
}
#[test]
fn test_write_trait() {
let mut handler = RotatingFileHandler::new("test_case_4_log.txt", 50, 1, None).unwrap();
write!(handler, "Hello, world!").unwrap();
write!(handler, " More data.").unwrap();
let content = fs::read_to_string("test_case_4_log.txt").unwrap();
assert_eq!(content, "Hello, world! More data.");
let _ = fs::remove_file("test_case_4_log.txt");
}
}