//! Audit logging functionality use crate::utils::{FileError, Result}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::io::Write; use std::path::Path; /// Represents an action that can be logged #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "action")] pub enum AuditAction { #[serde(rename = "directory_selected")] DirectorySelected { path: String }, #[serde(rename = "rename_file")] RenameFile { old_path: String, new_path: String, old_name: String, new_name: String, }, #[serde(rename = "move_file")] MoveFile { original_path: String, new_path: String, folder: String, }, #[serde(rename = "map_files")] MapFiles { directory: String, renamed_count: u32, error_count: u32, }, #[serde(rename = "tag_file")] TagFile { file_path: String, tag: String }, #[serde(rename = "untag_file")] UntagFile { file_path: String, tag: String }, } /// Logger for audit events pub struct AuditLogger { directory: String, } impl AuditLogger { /// Create a new AuditLogger for a directory pub fn new(directory: &str) -> Self { Self { directory: directory.to_string(), } } /// Log an audit event pub fn log_event(&self, action: AuditAction) -> Result<()> { let timestamp = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true); let entry = AuditEntry { timestamp, action, details: HashMap::new(), }; let audit_path = Path::new(&self.directory).join(".audit"); let line = serde_json::to_string(&entry).map_err(|e| FileError::Io(std::io::Error::other(e)))?; fs::OpenOptions::new() .create(true) .append(true) .open(&audit_path) .map_err(FileError::Io)? .write_all(format!("{}\n", line).as_bytes()) .map_err(FileError::Io)?; Ok(()) } } /// Represents a single audit log entry #[derive(Debug, Serialize, Deserialize)] struct AuditEntry { timestamp: String, action: AuditAction, details: HashMap, } #[cfg(test)] mod tests { use super::*; use std::fs::remove_dir_all; use tempfile::tempdir; #[test] fn test_audit_logger() { let temp_dir = tempdir().unwrap(); let logger = AuditLogger::new(temp_dir.path().to_str().unwrap()); // Test directory selection logging let action = AuditAction::DirectorySelected { path: temp_dir.path().to_string_lossy().to_string(), }; logger.log_event(action).unwrap(); // Verify .audit file was created let audit_path = temp_dir.path().join(".audit"); assert!(audit_path.exists()); // Read and verify content let content = fs::read_to_string(&audit_path).unwrap(); assert!(content.contains("directory_selected")); remove_dir_all(temp_dir.path()).unwrap(); } }