1use std::collections::HashSet;
2use std::io::BufRead;
3use std::path::PathBuf;
4
5use crate::Hashes;
6use crate::config::{Compression, Config};
7use anyhow::Result;
8use jiff::Timestamp;
9use serde::{Deserialize, Serialize};
10use uuid::Uuid;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum Action {
15 Add {
16 file: AuditFile,
17 compression: Compression,
18 #[serde(default, skip_serializing_if = "Option::is_none")]
19 message: Option<String>,
20 },
21 Init {
22 settings: Config,
23 project_path: PathBuf,
24 },
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct AuditFile {
29 pub path: PathBuf,
30 pub hashes: Hashes,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct AuditEntry {
35 pub operation_id: String,
36 #[serde(with = "jiff::fmt::serde::timestamp::second::required")]
37 pub timestamp: Timestamp,
38 pub user: String,
39 pub action: Action,
40}
41
42impl AuditEntry {
43 pub fn new_add(
44 operation_id: Uuid,
45 file: AuditFile,
46 compression: Compression,
47 message: Option<String>,
48 ) -> Self {
49 let timestamp = Timestamp::now();
50 let user = whoami::username().unwrap_or_else(|_| "unknown".to_string());
51
52 Self {
53 operation_id: operation_id.to_string(),
54 timestamp,
55 user,
56 action: Action::Add {
57 file,
58 compression,
59 message,
60 },
61 }
62 }
63
64 pub fn new_init(operation_id: Uuid, config: Config, project_path: PathBuf) -> Self {
65 let timestamp = Timestamp::now();
66 let user = whoami::username().unwrap_or_else(|_| "unknown".to_string());
67 let project_path = project_path.canonicalize().unwrap_or(project_path);
68
69 Self {
70 operation_id: operation_id.to_string(),
71 timestamp,
72 user,
73 action: Action::Init {
74 settings: config,
75 project_path,
76 },
77 }
78 }
79}
80
81pub fn parse_audit_log(
87 reader: impl BufRead,
88 only_files: &HashSet<PathBuf>,
89) -> Result<Vec<AuditEntry>> {
90 reader
91 .lines()
92 .map(|line| line.map_err(anyhow::Error::from))
93 .filter_map(|line| match line {
94 Ok(l) if l.trim().is_empty() => None,
95 other => Some(other),
96 })
97 .map(|line| Ok(serde_json::from_str::<AuditEntry>(&line?)?))
98 .filter(|entry| match entry {
99 Ok(e) => {
100 if only_files.is_empty() {
101 true
102 } else {
103 match &e.action {
104 Action::Add { file, .. } => only_files.contains(&file.path),
105 Action::Init { .. } => false,
106 }
107 }
108 }
109 Err(_) => true, })
111 .collect()
112}