use serde::{Serialize, Deserialize};
use tokio::fs;
use tokio::io::AsyncWriteExt;
use std::path::PathBuf;
use chrono::{DateTime, Local};
use crate::fs_ops::RenameExecutionResult;
use crate::history::RenameAction;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AuditEntry {
pub timestamp: DateTime<Local>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
pub working_directory: PathBuf,
pub command: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
pub successful_count: usize,
pub skipped_count: usize,
pub error_count: usize,
pub successful: Vec<(PathBuf, PathBuf)>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub skipped: Vec<(PathBuf, String)>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub errors: Vec<(PathBuf, String)>,
}
const AUDIT_FILE: &str = ".fren_audit.log";
fn get_current_user() -> Option<String> {
#[cfg(unix)]
{
std::env::var("USER").ok()
.or_else(|| std::env::var("USERNAME").ok())
}
#[cfg(windows)]
{
std::env::var("USERNAME").ok()
}
#[cfg(not(any(unix, windows)))]
{
None
}
}
pub async fn log_audit_entry(
command: &str,
pattern: Option<String>,
working_directory: PathBuf,
successful: Vec<(PathBuf, PathBuf)>,
skipped: Vec<(PathBuf, String)>,
errors: Vec<(PathBuf, String)>,
) -> Result<(), Box<dyn std::error::Error>> {
if working_directory.as_os_str().is_empty() {
return Err("Working directory cannot be empty".into());
}
fs::create_dir_all(&working_directory).await?;
let audit_path = working_directory.join(AUDIT_FILE);
eprintln!("DEBUG: working_directory = {:?}", working_directory);
eprintln!("DEBUG: audit_path = {:?}", audit_path);
eprintln!("DEBUG: audit_path exists before write = {}", audit_path.exists());
eprintln!("DEBUG: working_directory exists = {}", working_directory.exists());
eprintln!("DEBUG: working_directory is_dir = {}", working_directory.is_dir());
let entry = AuditEntry {
timestamp: Local::now(),
user: get_current_user(),
working_directory: working_directory.clone(),
command: command.to_string(),
pattern,
successful_count: successful.len(),
skipped_count: skipped.len(),
error_count: errors.len(),
successful,
skipped,
errors,
};
let json = serde_json::to_string(&entry)?;
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(&audit_path)
.await
.map_err(|e| format!("Failed to open audit file at {:?}: {}", audit_path, e))?;
file.write_all(json.as_bytes()).await
.map_err(|e| format!("Failed to write to audit file: {}", e))?;
file.write_all(b"\n").await
.map_err(|e| format!("Failed to write newline: {}", e))?;
file.sync_all().await
.map_err(|e| format!("Failed to sync audit file: {}", e))?;
drop(file);
eprintln!("DEBUG: audit_path exists after write = {}", audit_path.exists());
let metadata_result = fs::metadata(&audit_path).await;
eprintln!("DEBUG: fs::metadata result = {:?}", metadata_result);
if metadata_result.is_err() {
return Err(format!("Audit file was not created at {:?} (after sync delay). Metadata error: {:?}", audit_path, metadata_result.err()).into());
}
Ok(())
}
pub async fn read_audit_log() -> Result<Vec<AuditEntry>, Box<dyn std::error::Error>> {
let audit_path = std::env::current_dir()?.join(AUDIT_FILE);
if fs::metadata(&audit_path).await.is_err() {
return Ok(Vec::new());
}
let content = fs::read_to_string(&audit_path).await?;
let mut entries = Vec::new();
for line in content.lines() {
if line.trim().is_empty() {
continue;
}
let entry: AuditEntry = serde_json::from_str(line)?;
entries.push(entry);
}
entries.reverse();
Ok(entries)
}
pub async fn log_audit_from_result(
command: &str,
pattern: Option<String>,
working_directory: PathBuf,
result: &RenameExecutionResult,
) -> Result<(), Box<dyn std::error::Error>> {
let successful: Vec<(PathBuf, PathBuf)> = result.successful.iter()
.map(|action: &RenameAction| (action.old_path.clone(), action.new_path.clone()))
.collect();
log_audit_entry(
command,
pattern,
working_directory,
successful,
result.skipped.clone(),
result.errors.clone(),
).await
}
pub async fn clear_audit_log() -> Result<(), Box<dyn std::error::Error>> {
let audit_path = std::env::current_dir()?.join(AUDIT_FILE);
if fs::metadata(&audit_path).await.is_ok() {
fs::remove_file(&audit_path).await?;
}
Ok(())
}