use std::ffi::OsStr;
use std::fs;
use std::fs::Metadata;
use std::path::PathBuf;
use std::sync::LazyLock;
use error_stack::ResultExt;
use regex::Regex;
use super::constants::BYTES_PER_UNIT;
use super::constants::LOG_EXTENSION;
use super::constants::LOG_PREFIX;
use super::constants::UNITS;
use crate::error::Error;
use crate::error::Result;
static APP_LOG_REGEX: LazyLock<Option<Regex>> =
LazyLock::new(|| Regex::new(r"^bevy_brp_mcp_(.+?)_port\d+_(\d+)_\d+\.log$").ok());
#[derive(Debug, Clone)]
pub(super) struct LogFileEntry {
pub(super) filename: String,
pub(super) app_name: String,
pub(super) timestamp: String,
pub(super) path: PathBuf,
pub(super) metadata: Metadata,
}
pub(super) fn is_valid_log_filename(filename: &str) -> bool {
filename.starts_with(LOG_PREFIX) && filename.ends_with(LOG_EXTENSION)
}
pub(super) fn parse_app_log_filename(filename: &str) -> Option<(String, String)> {
if !is_valid_log_filename(filename) {
return None;
}
let regex = APP_LOG_REGEX.as_ref()?;
if let Some(captures) = regex.captures(filename) {
let app_name = captures.get(1)?.as_str().to_string();
let timestamp = captures.get(2)?.as_str().to_string();
return Some((app_name, timestamp));
}
None
}
pub(super) fn parse_log_filename(filename: &str) -> Option<(String, String)> {
if let Some(result) = parse_app_log_filename(filename) {
return Some(result);
}
if !is_valid_log_filename(filename) {
return None;
}
let parts: Vec<&str> = filename
.trim_start_matches(LOG_PREFIX)
.trim_end_matches(LOG_EXTENSION)
.rsplitn(2, '_')
.collect();
if parts.len() != 2 {
return None;
}
let timestamp_str = parts[0].to_string();
let app_name = parts[1].to_string();
Some((app_name, timestamp_str))
}
pub(super) fn format_bytes(bytes: u64) -> String {
#[allow(
clippy::cast_precision_loss,
reason = "log file sizes never approach 2^53, where f64 loses integer precision"
)]
let mut size = bytes as f64;
let mut unit_index = 0;
while size >= BYTES_PER_UNIT && unit_index < UNITS.len() - 1 {
size /= BYTES_PER_UNIT;
unit_index += 1;
}
let unit = UNITS[unit_index];
if unit_index == 0 {
format!("{bytes} {unit}")
} else {
format!("{size:.2} {unit}")
}
}
pub(super) fn get_log_directory() -> PathBuf { std::env::temp_dir() }
pub(super) fn get_log_file_path(filename: &str) -> PathBuf { get_log_directory().join(filename) }
pub(super) fn iterate_app_log_files<F>(filter: F) -> Result<Vec<LogFileEntry>>
where
F: Fn(&LogFileEntry) -> bool,
{
let temp_dir = get_log_directory();
let mut log_entries = Vec::new();
let entries = fs::read_dir(&temp_dir)
.change_context(Error::FileOperation(
"Failed to read temp directory".to_string(),
))
.attach(format!("Path: {}", temp_dir.display()))?;
for entry in entries {
let entry = entry
.change_context(Error::FileOperation(
"Failed to read directory entry".to_string(),
))
.attach(format!("Directory: {}", temp_dir.display()))?;
let path = entry.path();
let filename = path.file_name().and_then(OsStr::to_str).unwrap_or("");
if let Some((app_name, timestamp)) = parse_app_log_filename(filename) {
let metadata = entry
.metadata()
.change_context(Error::FileOperation(
"Failed to get file metadata".to_string(),
))
.attach(format!("Path: {}", path.display()))?;
let log_entry = LogFileEntry {
filename: filename.to_string(),
app_name,
timestamp,
path,
metadata,
};
if filter(&log_entry) {
log_entries.push(log_entry);
}
}
}
Ok(log_entries)
}
pub(super) fn iterate_log_files<F>(filter: F) -> Result<Vec<LogFileEntry>>
where
F: Fn(&LogFileEntry) -> bool,
{
let temp_dir = get_log_directory();
let mut log_entries = Vec::new();
let entries = fs::read_dir(&temp_dir)
.change_context(Error::FileOperation(
"Failed to read temp directory".to_string(),
))
.attach(format!("Path: {}", temp_dir.display()))?;
for entry in entries {
let entry = entry
.change_context(Error::FileOperation(
"Failed to read directory entry".to_string(),
))
.attach(format!("Directory: {}", temp_dir.display()))?;
let path = entry.path();
let filename = path.file_name().and_then(OsStr::to_str).unwrap_or("");
if let Some((app_name, timestamp)) = parse_log_filename(filename) {
let metadata = entry
.metadata()
.change_context(Error::FileOperation(
"Failed to get file metadata".to_string(),
))
.attach(format!("Path: {}", path.display()))?;
let log_entry = LogFileEntry {
filename: filename.to_string(),
app_name,
timestamp,
path,
metadata,
};
if filter(&log_entry) {
log_entries.push(log_entry);
}
}
}
Ok(log_entries)
}