use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use regex::Regex;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use crate::{FileEvent, OutputFormat, SortBy};
use crate::utils::{format_size, parse_time};
pub struct Query {
log_file: PathBuf,
since: Option<String>,
until: Option<String>,
pids: Option<Vec<u32>>,
cmd: Option<String>,
users: Option<Vec<String>>,
event_types: Option<Vec<String>>,
min_size: Option<i64>,
format: OutputFormat,
sort: SortBy,
}
impl Query {
pub fn new(
log_file: PathBuf,
since: Option<String>,
until: Option<String>,
pids: Option<Vec<u32>>,
cmd: Option<String>,
users: Option<Vec<String>>,
event_types: Option<Vec<String>>,
min_size: Option<i64>,
format: OutputFormat,
sort: SortBy,
) -> Self {
Self {
log_file,
since,
until,
pids,
cmd,
users,
event_types,
min_size,
format,
sort,
}
}
pub async fn execute(&self) -> Result<()> {
let since_time = self.since.as_ref()
.map(|s| parse_time(s))
.transpose()?;
let until_time = self.until.as_ref()
.map(|s| parse_time(s))
.transpose()?;
let cmd_regex = self.cmd.as_ref()
.map(|c| Regex::new(&c.replace("*", ".*")))
.transpose()?;
let events = self.read_events(
since_time,
until_time,
cmd_regex,
)?;
let sorted_events = self.sort_events(events);
self.output_events(&sorted_events)?;
Ok(())
}
fn read_events(
&self,
since_time: Option<DateTime<Utc>>,
until_time: Option<DateTime<Utc>>,
cmd_regex: Option<Regex>,
) -> Result<Vec<FileEvent>> {
let file = File::open(&self.log_file)
.with_context(|| format!("Failed to open log file: {}", self.log_file.display()))?;
let reader = BufReader::new(file);
let mut events = Vec::new();
for line in reader.lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let event: FileEvent = match serde_json::from_str(&line) {
Ok(e) => e,
Err(_) => continue,
};
if let Some(ref since) = since_time {
if event.time < *since {
continue;
}
}
if let Some(ref until) = until_time {
if event.time > *until {
continue;
}
}
if let Some(ref pids) = self.pids {
if !pids.contains(&event.pid) {
continue;
}
}
if let Some(ref regex) = cmd_regex {
if !regex.is_match(&event.cmd) {
continue;
}
}
if let Some(ref users) = self.users {
if !users.contains(&event.user) {
continue;
}
}
if let Some(ref types) = self.event_types {
if !types.contains(&event.event_type) {
continue;
}
}
if let Some(min) = self.min_size {
if event.size_change.abs() < min {
continue;
}
}
events.push(event);
}
Ok(events)
}
fn sort_events(&self, mut events: Vec<FileEvent>) -> Vec<FileEvent> {
match self.sort {
SortBy::Time => {
events.sort_by(|a, b| a.time.cmp(&b.time));
}
SortBy::Size => {
events.sort_by(|a, b| b.size_change.abs().cmp(&a.size_change.abs()));
}
SortBy::Pid => {
events.sort_by(|a, b| a.pid.cmp(&b.pid));
}
}
events
}
fn output_events(&self, events: &[FileEvent]) -> Result<()> {
if events.is_empty() {
println!("No matching events found");
return Ok(());
}
match self.format {
OutputFormat::Human => {
for event in events {
println!("{}", event.to_human_string());
}
}
OutputFormat::Json => {
for event in events {
println!("{}", serde_json::to_string(event)?);
}
}
OutputFormat::Csv => {
println!("time,type,path,pid,cmd,user,size_change,size_change_str");
for event in events {
let size_human = format_size(event.size_change);
let size_prefix = if event.size_change >= 0 { "+" } else { "" };
println!(
"{},{},{},{},{},{},{},{}{}",
event.time.to_rfc3339(),
event.event_type,
event.path.display(),
event.pid,
event.cmd,
event.user,
event.size_change,
size_prefix,
size_human
);
}
}
}
Ok(())
}
}