use std::fs;
use std::path::{Path, PathBuf};
use anyhow::Context;
use freeswitch_log_parser::{log_rotation_stamp, stamp_lower_bound, stamp_upper_bound};
pub struct LogFile {
pub path: PathBuf,
pub date: Option<String>,
pub size: u64,
}
pub fn discover_log_files(dir: &Path) -> anyhow::Result<Vec<LogFile>> {
let mut files = Vec::new();
let listing =
fs::read_dir(dir).with_context(|| format!("listing log directory {}", dir.display()))?;
for entry in listing {
let entry = entry.with_context(|| format!("reading an entry of {}", dir.display()))?;
let path = entry.path();
let name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_string(),
None => continue,
};
if !name.starts_with("freeswitch.log") {
continue;
}
let meta = entry
.metadata()
.with_context(|| format!("stat {}", path.display()))?;
if !meta.is_file() {
continue;
}
let date = log_rotation_stamp(&name).map(str::to_string);
files.push(LogFile {
path,
date,
size: meta.len(),
});
}
files.sort_by(|a, b| a.date.cmp(&b.date));
Ok(files)
}
pub fn filter_files_by_date<'a>(
files: &'a [LogFile],
from: Option<&str>,
until: Option<&str>,
) -> Vec<&'a LogFile> {
let from_norm = from.map(stamp_lower_bound);
let until_norm = until.map(stamp_upper_bound);
files
.iter()
.enumerate()
.filter(|(i, f)| {
let Some(ref file_date) = f.date else {
return true;
};
if let Some(ref until) = until_norm {
if file_date.as_str() > until.as_str() && *i > 0 {
if let Some(ref prev_date) = files[*i - 1].date {
if prev_date.as_str() > until.as_str() {
return false;
}
}
}
}
if let Some(ref from) = from_norm {
if file_date.as_str() < from.as_str() {
if *i + 1 < files.len() {
if let Some(ref next_date) = files[*i + 1].date {
if next_date.as_str() >= from.as_str() {
return true;
}
} else {
return true;
}
}
return false;
}
}
true
})
.map(|(_, f)| f)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn day_files(days: &[Option<&str>]) -> Vec<LogFile> {
days.iter()
.map(|d| LogFile {
path: PathBuf::from("freeswitch.log"),
date: d.map(|d| format!("2026-03-{d}-00-00-00")),
size: 0,
})
.collect()
}
fn selected(files: &[LogFile], from: Option<&str>, until: Option<&str>) -> Vec<String> {
filter_files_by_date(files, from, until)
.iter()
.map(|f| match &f.date {
Some(d) => d[8..10].to_string(),
None => "live".to_string(),
})
.collect()
}
#[test]
fn unbounded_selection_keeps_everything() {
let files = day_files(&[Some("08"), Some("09"), None]);
assert_eq!(selected(&files, None, None), ["08", "09", "live"]);
}
#[test]
fn from_reaches_back_one_file() {
let files = day_files(&[Some("08"), Some("09"), Some("10"), Some("11")]);
assert_eq!(
selected(&files, Some("2026-03-10"), None),
["09", "10", "11"]
);
}
#[test]
fn from_drops_files_that_end_before_the_next_one_starts() {
let files = day_files(&[Some("08"), Some("09"), Some("10"), Some("11")]);
assert_eq!(selected(&files, Some("2026-03-11"), None), ["10", "11"]);
}
#[test]
fn until_keeps_the_file_the_bound_falls_inside() {
let files = day_files(&[Some("08"), Some("09"), Some("10"), Some("11")]);
assert_eq!(
selected(&files, None, Some("2026-03-09")),
["08", "09", "10"]
);
}
#[test]
fn the_active_log_is_never_excluded_by_date() {
let files = day_files(&[Some("08"), Some("09"), None]);
assert_eq!(
selected(&files, None, Some("2026-03-08")),
["08", "09", "live"]
);
assert_eq!(selected(&files, Some("2026-03-20"), None), ["09", "live"]);
}
#[test]
fn a_month_bound_spans_the_whole_month() {
let files = day_files(&[Some("08"), Some("09")]);
assert_eq!(
selected(&files, Some("2026-03"), Some("2026-03")),
["08", "09"]
);
assert!(selected(&files, Some("2026-04"), Some("2026-04")).is_empty());
}
}