Skip to main content
← dvs documentation Rust API reference

dvs/
audit.rs

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
81/// Parse an audit log into entries, optionally filtered to specific files.
82///
83/// An empty `only_files` set returns every entry, that is the whole log. A
84/// non-empty set keeps only `Add` entries whose path is in the set and drops
85/// all `Init` entries.
86pub 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, // propagate errors
110        })
111        .collect()
112}