use crate::log_data::{LogData, LogEntryParams};
use chrono::{DateTime, FixedOffset};
use log::{error, warn};
use regex::Regex;
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom};
use std::path::PathBuf;
use std::sync::{Arc, Mutex as StdMutex};
pub async fn tail_file(
file_path: &PathBuf,
count: isize,
regex_pattern: &str,
date_format: &str,
log_data: &Arc<StdMutex<LogData>>,
last_processed_line: Option<usize>,
progress_callback: impl Fn(f64) + Send,
) -> std::io::Result<Option<usize>> {
let file = OpenOptions::new().read(true).open(file_path)?;
let metadata = file.metadata()?;
let file_size = metadata.len() as f64;
let mut reader = BufReader::new(file);
let mut last_processed = last_processed_line;
if let Some(ref last_line) = last_processed_line {
set_reader_to_last_processed_line(&mut reader, *last_line, &progress_callback, file_size)
.await?;
} else if count > 0 {
process_last_n_lines(
&mut reader,
count,
regex_pattern,
date_format,
log_data,
&mut last_processed,
&progress_callback,
file_size,
)
.await?;
} else if count == -1 {
process_all_lines_from_start(
&mut reader,
regex_pattern,
date_format,
log_data,
&mut last_processed,
&progress_callback,
file_size,
)
.await?;
} else {
set_last_processed_to_last_line(&mut reader, &mut last_processed).await?;
}
process_new_lines(
&mut reader,
regex_pattern,
date_format,
log_data,
&mut last_processed,
&progress_callback,
file_size,
)
.await?;
Ok(last_processed)
}
async fn set_last_processed_to_last_line(
reader: &mut BufReader<File>,
last_processed: &mut Option<usize>,
) -> std::io::Result<()> {
let mut buffer = String::new();
reader.seek(SeekFrom::Start(0))?;
reader.read_to_string(&mut buffer)?;
let lines: Vec<&str> = buffer.lines().collect();
*last_processed = Some(lines.len());
Ok(())
}
async fn set_reader_to_last_processed_line(
reader: &mut BufReader<File>,
last_line_number: usize,
progress_callback: &impl Fn(f64),
file_size: f64,
) -> std::io::Result<()> {
let mut current_line = 0;
let mut buffer = String::new();
let mut processed_bytes = 0;
while current_line < last_line_number {
let bytes_read = reader.read_line(&mut buffer)?;
if bytes_read == 0 {
break; }
processed_bytes += bytes_read;
current_line += 1;
buffer.clear();
progress_callback((processed_bytes as f64 / file_size).min(100.0));
}
Ok(())
}
async fn process_last_n_lines(
reader: &mut BufReader<File>,
count: isize,
regex_pattern: &str,
date_format: &str,
log_data: &Arc<StdMutex<LogData>>,
last_processed: &mut Option<usize>,
progress_callback: &impl Fn(f64),
_file_size: f64,
) -> std::io::Result<()> {
let mut lines = Vec::new();
let mut line = String::new();
while reader.read_line(&mut line)? > 0 {
lines.push(line.clone());
line.clear();
}
let start = if lines.len() > count as usize {
lines.len() - count as usize
} else {
0
};
let total_lines = count as usize; let mut processed_lines = 0;
for (index, line) in lines[start..].iter().enumerate() {
process_line(line, regex_pattern, date_format, log_data).await?;
processed_lines += 1;
progress_callback((processed_lines as f64 / total_lines as f64).min(1.0));
*last_processed = Some(start + index + 1);
}
reader.seek(SeekFrom::Start(0))?;
for _ in 0..*last_processed.as_ref().unwrap_or(&0) {
let mut line = String::new();
reader.read_line(&mut line)?;
}
Ok(())
}
async fn process_all_lines_from_start(
reader: &mut BufReader<File>,
regex_pattern: &str,
date_format: &str,
log_data: &Arc<StdMutex<LogData>>,
last_processed: &mut Option<usize>,
progress_callback: &impl Fn(f64),
file_size: f64,
) -> std::io::Result<()> {
reader.seek(SeekFrom::Start(0))?;
let mut processed_bytes = 0;
let mut line_number = 0;
let mut line = String::new();
while reader.read_line(&mut line)? > 0 {
process_line(&line, regex_pattern, date_format, log_data).await?;
processed_bytes += line.len();
line.clear();
progress_callback((processed_bytes as f64 / file_size).min(1.0));
line_number += 1;
*last_processed = Some(line_number);
}
Ok(())
}
async fn process_new_lines(
reader: &mut BufReader<File>,
regex_pattern: &str,
date_format: &str,
log_data: &Arc<StdMutex<LogData>>,
last_processed: &mut Option<usize>,
progress_callback: &impl Fn(f64),
file_size: f64,
) -> std::io::Result<()> {
let mut line = String::new();
let mut processed_bytes = 0;
let mut line_number = last_processed.unwrap_or(0);
while reader.read_line(&mut line)? > 0 {
process_line(&line, regex_pattern, date_format, log_data).await?;
processed_bytes += line.len();
line.clear();
progress_callback((processed_bytes as f64 / file_size).min(1.0));
line_number += 1;
*last_processed = Some(line_number);
}
Ok(())
}
pub async fn process_line(
line: &str,
regex_pattern: &str,
date_format: &str,
log_data: &Arc<StdMutex<LogData>>,
) -> std::io::Result<()> {
let re = match Regex::new(regex_pattern) {
Ok(re) => re,
Err(e) => {
error!("Failed to compile regex pattern '{}': {}", regex_pattern, e);
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Invalid regex pattern: {}", e),
));
}
};
let caps = match re.captures(line) {
Some(caps) => caps,
None => {
error!(
"Line does not match regex pattern '{}': {}",
regex_pattern,
line.trim()
);
return Ok(()); }
};
let (ip, datetime_str, request_domain, request_type, url) = match extract_captures_safe(&caps) {
Ok(data) => data,
Err(e) => {
error!(
"Failed to extract captures from line: {} (Error: {})",
line.trim(),
e
);
return Ok(()); }
};
if ip.is_empty() {
error!("Empty IP address in line: {}", line.trim());
return Ok(());
}
let datetime = match parse_datetime_safe(&datetime_str, date_format) {
Ok(dt) => dt,
Err(e) => {
error!(
"Failed to parse datetime '{}' with format '{}': {}",
datetime_str, date_format, e
);
return Ok(()); }
};
let (status_code, response_size, response_time, user_agent) =
extract_additional_data_safe(line);
let mut log_data = log_data
.lock()
.expect("Failed to acquire log data lock for entry addition");
let params = LogEntryParams {
ip,
url,
log_line: line.to_string(),
timestamp: datetime.timestamp(),
request_type,
request_domain,
status_code,
response_size,
response_time,
user_agent,
};
log_data.add_entry(params);
Ok(())
}
fn extract_captures_safe(
caps: ®ex::Captures,
) -> Result<(String, String, String, String, String), String> {
let ip = caps.get(1).map_or("", |m| m.as_str()).to_string();
let datetime_str = caps.get(2).map_or("", |m| m.as_str()).to_string();
let request_domain = caps.get(3).map_or("", |m| m.as_str()).to_string();
let request_type = caps.get(4).map_or("", |m| m.as_str()).to_string();
let url = caps.get(5).map_or("", |m| m.as_str()).to_string();
if ip.is_empty() {
return Err("IP address is empty".to_string());
}
if datetime_str.is_empty() {
return Err("Datetime is empty".to_string());
}
if request_type.is_empty() {
return Err("Request type is empty".to_string());
}
if url.is_empty() {
return Err("URL is empty".to_string());
}
Ok((ip, datetime_str, request_domain, request_type, url))
}
fn parse_datetime_safe(
datetime_str: &str,
date_format: &str,
) -> Result<DateTime<FixedOffset>, String> {
match DateTime::parse_from_str(datetime_str, date_format) {
Ok(dt) => Ok(dt),
Err(e1) => {
match DateTime::parse_from_str(datetime_str, "%d/%b/%Y:%H:%M:%S %z") {
Ok(dt) => Ok(dt),
Err(e2) => {
match DateTime::parse_from_str(datetime_str, "%Y-%m-%d %H:%M:%S %z") {
Ok(dt) => Ok(dt),
Err(e3) => {
Err(format!("Failed to parse datetime '{}' with formats '{}', '%d/%b/%Y:%H:%M:%S %z', '%Y-%m-%d %H:%M:%S %z'. Errors: {}, {}, {}",
datetime_str, date_format, e1, e2, e3))
}
}
}
}
}
}
}
fn extract_additional_data_safe(
line: &str,
) -> (Option<u16>, Option<u64>, Option<f64>, Option<String>) {
let mut status_code = None;
let mut response_size = None;
let mut user_agent = None;
if !line.contains('"') || !line.contains('[') || !line.contains(']') {
return (None, None, Some(0.1), None);
}
let quote_positions: Vec<usize> = line.char_indices()
.filter(|(_, c)| *c == '"')
.map(|(i, _)| i)
.collect();
if quote_positions.len() >= 4 {
let after_second_quote = &line[quote_positions[1] + 1..];
let parts: Vec<&str> = after_second_quote.split_whitespace().collect();
if parts.len() >= 2 {
if let Ok(code) = parts[0].parse::<u16>() {
status_code = Some(code);
}
if let Ok(size) = parts[1].parse::<u64>() {
response_size = Some(size);
}
}
}
if quote_positions.len() >= 6 {
let ua_start = quote_positions[quote_positions.len() - 2] + 1;
let ua_end = quote_positions[quote_positions.len() - 1];
let ua = &line[ua_start..ua_end];
if !ua.trim().is_empty() && ua != "-" {
user_agent = Some(ua.trim().to_string());
}
}
let response_time = Some(0.1);
(status_code, response_size, response_time, user_agent)
}