use anyhow::{Result, anyhow}; use chrono::NaiveDate; use flate2::read::GzDecoder; use std::fs::File; use std::io::{BufRead, BufReader, Read}; use std::path::PathBuf; /// Enum-based reader to avoid Box heap allocation and dynamic dispatch pub enum LogReader { Plain(BufReader), Gzip(BufReader>), } impl Read for LogReader { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { match self { LogReader::Plain(r) => r.read(buf), LogReader::Gzip(r) => r.read(buf), } } } impl BufRead for LogReader { fn fill_buf(&mut self) -> std::io::Result<&[u8]> { match self { LogReader::Plain(r) => r.fill_buf(), LogReader::Gzip(r) => r.fill_buf(), } } fn consume(&mut self, amt: usize) { match self { LogReader::Plain(r) => r.consume(amt), LogReader::Gzip(r) => r.consume(amt), } } } /// Discovers log files for a given date range pub struct LogFileDiscovery { base_dir: PathBuf, filename: String, } impl LogFileDiscovery { pub fn new(base_dir: PathBuf, filename: String) -> Self { Self { base_dir, filename } } /// Returns an iterator over all log files in the date range pub fn discover(&self, from: NaiveDate, to: NaiveDate) -> Result> { let mut files = Vec::new(); let mut current = from; while current <= to { if let Some(log_file) = self.find_log_for_date(current)? { files.push(log_file); } current = current.succ_opt().ok_or_else(|| anyhow!("Date overflow"))?; } Ok(files) } fn find_log_for_date(&self, date: NaiveDate) -> Result> { // Build path: /yyyy/mm/dd/.gz or let date_path = self .base_dir .join(date.format("%Y").to_string()) .join(date.format("%m").to_string()) .join(date.format("%d").to_string()); // Try gzipped first let gz_path = date_path.join(format!("{}.gz", self.filename)); if gz_path.exists() { return Ok(Some(LogFile { path: gz_path, compressed: true, })); } // Try uncompressed let plain_path = date_path.join(&self.filename); if plain_path.exists() { return Ok(Some(LogFile { path: plain_path, compressed: false, })); } // No file found for this date Ok(None) } } #[derive(Debug)] pub struct LogFile { pub path: PathBuf, pub compressed: bool, } impl LogFile { /// Returns a buffered reader for this log file, handling compression transparently pub fn reader(&self) -> Result { let file = File::open(&self.path)?; if self.compressed { let decoder = GzDecoder::new(file); Ok(LogReader::Gzip(BufReader::new(decoder))) } else { Ok(LogReader::Plain(BufReader::new(file))) } } } /// For reading a single file directly (e.g., for testing) pub fn read_log_file(path: &str) -> Result { let file = File::open(path)?; if path.ends_with(".gz") { let decoder = GzDecoder::new(file); Ok(LogReader::Gzip(BufReader::new(decoder))) } else { Ok(LogReader::Plain(BufReader::new(file))) } }