vtcode_core/audit/
file_conflict_log.rs1use crate::utils::error_messages::ERR_CREATE_AUDIT_DIR;
4use crate::utils::file_utils::ensure_dir_exists_sync;
5use anyhow::{Context, Result};
6use chrono::{DateTime, Local};
7use serde::{Deserialize, Serialize};
8use std::fs::OpenOptions;
9use std::io::BufWriter;
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct FileConflictAuditEvent {
15 pub timestamp: DateTime<Local>,
16 pub path: PathBuf,
17 pub reason: String,
18 pub file_exists: bool,
19 pub size_bytes: Option<u64>,
20 pub sha256: Option<String>,
21}
22
23pub struct FileConflictAuditLog {
25 writer: Option<BufWriter<std::fs::File>>,
26 log_path: PathBuf,
27}
28
29impl FileConflictAuditLog {
30 pub fn new(audit_dir: PathBuf) -> Result<Self> {
32 ensure_dir_exists_sync(&audit_dir).context(ERR_CREATE_AUDIT_DIR)?;
33
34 let date = Local::now().format("%Y-%m-%d");
35 let log_path = audit_dir.join(format!("file-conflicts-{}.log", date));
36 Ok(Self {
37 writer: None,
38 log_path,
39 })
40 }
41
42 pub fn record(&mut self, event: &FileConflictAuditEvent) -> Result<()> {
43 use std::io::Write;
44
45 let json = serde_json::to_string(event)
46 .context("Failed to serialize file conflict audit event")?;
47 let writer = self.writer_mut()?;
48 writeln!(writer, "{json}").context("Failed to write file conflict audit event")?;
49 writer
50 .flush()
51 .context("Failed to flush file conflict audit log")?;
52 Ok(())
53 }
54
55 pub fn log_path(&self) -> &Path {
56 &self.log_path
57 }
58
59 fn writer_mut(&mut self) -> Result<&mut BufWriter<std::fs::File>> {
60 if self.writer.is_none() {
61 let file = OpenOptions::new()
62 .create(true)
63 .append(true)
64 .open(&self.log_path)
65 .with_context(|| {
66 format!(
67 "Failed to open file conflict audit log at {:?}",
68 self.log_path
69 )
70 })?;
71 self.writer = Some(BufWriter::new(file));
72 }
73
74 self.writer
75 .as_mut()
76 .context("file conflict audit log writer was not initialized")
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use tempfile::TempDir;
84
85 #[test]
86 fn defers_file_conflict_audit_log_creation_until_first_record() -> Result<()> {
87 let dir = TempDir::new()?;
88 let log = FileConflictAuditLog::new(dir.path().to_path_buf())?;
89 assert!(!log.log_path().exists());
90 Ok(())
91 }
92}