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 },
19 Init {
20 settings: Config,
21 project_path: PathBuf,
22 },
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct AuditFile {
27 pub path: PathBuf,
28 pub hashes: Hashes,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct AuditEntry {
33 pub operation_id: String,
34 #[serde(with = "jiff::fmt::serde::timestamp::second::required")]
35 pub timestamp: Timestamp,
36 pub user: String,
37 pub action: Action,
38}
39
40impl AuditEntry {
41 pub fn new_add(operation_id: Uuid, file: AuditFile, compression: Compression) -> Self {
42 let timestamp = Timestamp::now();
43 let user = whoami::username().unwrap_or_else(|_| "unknown".to_string());
44
45 Self {
46 operation_id: operation_id.to_string(),
47 timestamp,
48 user,
49 action: Action::Add { file, compression },
50 }
51 }
52
53 pub fn new_init(operation_id: Uuid, config: Config, project_path: PathBuf) -> Self {
54 let timestamp = Timestamp::now();
55 let user = whoami::username().unwrap_or_else(|_| "unknown".to_string());
56 let project_path = project_path.canonicalize().unwrap_or(project_path);
57
58 Self {
59 operation_id: operation_id.to_string(),
60 timestamp,
61 user,
62 action: Action::Init {
63 settings: config,
64 project_path,
65 },
66 }
67 }
68}
69
70pub fn parse_audit_log(
76 reader: impl BufRead,
77 only_files: &HashSet<PathBuf>,
78) -> Result<Vec<AuditEntry>> {
79 reader
80 .lines()
81 .map(|line| line.map_err(anyhow::Error::from))
82 .filter_map(|line| match line {
83 Ok(l) if l.trim().is_empty() => None,
84 other => Some(other),
85 })
86 .map(|line| Ok(serde_json::from_str::<AuditEntry>(&line?)?))
87 .filter(|entry| match entry {
88 Ok(e) => {
89 if only_files.is_empty() {
90 true
91 } else {
92 match &e.action {
93 Action::Add { file, .. } => only_files.contains(&file.path),
94 Action::Init { .. } => false,
95 }
96 }
97 }
98 Err(_) => true, })
100 .collect()
101}