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    },
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
70/// Parse an audit log into entries, optionally filtered to specific files.
71///
72/// An empty `only_files` set returns every entry, that is the whole log. A
73/// non-empty set keeps only `Add` entries whose path is in the set and drops
74/// all `Init` entries.
75pub 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, // propagate errors
99        })
100        .collect()
101}