convergio_backup/
types.rs1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, thiserror::Error)]
7pub enum BackupError {
8 #[error("database error: {0}")]
9 Db(#[from] rusqlite::Error),
10
11 #[error("pool error: {0}")]
12 Pool(#[from] r2d2::Error),
13
14 #[error("io error: {0}")]
15 Io(#[from] std::io::Error),
16
17 #[error("json error: {0}")]
18 Json(#[from] serde_json::Error),
19
20 #[error("snapshot not found: {0}")]
21 SnapshotNotFound(String),
22
23 #[error("invalid config: {0}")]
24 InvalidConfig(String),
25
26 #[error("restore failed: {0}")]
27 RestoreFailed(String),
28}
29
30pub type BackupResult<T> = Result<T, BackupError>;
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct RetentionRule {
35 pub table: String,
37 pub timestamp_column: String,
39 pub max_age_days: u32,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct SnapshotRecord {
46 pub id: String,
47 pub path: String,
48 pub size_bytes: i64,
49 pub checksum: String,
50 pub created_at: String,
51 pub node: String,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct OrgExportMeta {
57 pub org_id: String,
58 pub org_name: String,
59 pub exported_at: String,
60 pub node: String,
61 pub tables: Vec<String>,
62 pub row_counts: Vec<(String, i64)>,
63 pub version: String,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct PurgeEvent {
69 pub table: String,
70 pub rows_deleted: i64,
71 pub cutoff_date: String,
72 pub executed_at: String,
73}
74
75pub fn validate_sql_identifier(name: &str) -> BackupResult<()> {
80 if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
81 return Err(BackupError::InvalidConfig(format!(
82 "invalid SQL identifier: {name:?}"
83 )));
84 }
85 Ok(())
86}
87
88pub fn default_retention_rules() -> Vec<RetentionRule> {
90 vec![
91 RetentionRule {
92 table: "audit_log".into(),
93 timestamp_column: "created_at".into(),
94 max_age_days: 365,
95 },
96 RetentionRule {
97 table: "ipc_messages".into(),
98 timestamp_column: "created_at".into(),
99 max_age_days: 30,
100 },
101 ]
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 #[test]
109 fn default_rules_cover_required_tables() {
110 let rules = default_retention_rules();
111 assert_eq!(rules.len(), 2);
112 assert_eq!(rules[0].table, "audit_log");
113 assert_eq!(rules[0].max_age_days, 365);
114 assert_eq!(rules[1].table, "ipc_messages");
115 assert_eq!(rules[1].max_age_days, 30);
116 }
117
118 #[test]
119 fn snapshot_record_serializes() {
120 let rec = SnapshotRecord {
121 id: "snap-001".into(),
122 path: "/tmp/backup.db".into(),
123 size_bytes: 1024,
124 checksum: "abc123".into(),
125 created_at: "2026-04-03T00:00:00Z".into(),
126 node: "m5max".into(),
127 };
128 let json = serde_json::to_string(&rec).unwrap();
129 assert!(json.contains("snap-001"));
130 }
131
132 #[test]
133 fn purge_event_serializes() {
134 let ev = PurgeEvent {
135 table: "audit_log".into(),
136 rows_deleted: 42,
137 cutoff_date: "2025-04-03".into(),
138 executed_at: "2026-04-03T00:00:00Z".into(),
139 };
140 let json = serde_json::to_string(&ev).unwrap();
141 assert!(json.contains("audit_log"));
142 assert!(json.contains("42"));
143 }
144
145 #[test]
146 fn validate_sql_identifier_accepts_valid() {
147 assert!(validate_sql_identifier("audit_log").is_ok());
148 assert!(validate_sql_identifier("table1").is_ok());
149 assert!(validate_sql_identifier("created_at").is_ok());
150 }
151
152 #[test]
153 fn validate_sql_identifier_rejects_injection() {
154 assert!(validate_sql_identifier("").is_err());
155 assert!(validate_sql_identifier("table; DROP TABLE x").is_err());
156 assert!(validate_sql_identifier("audit-log").is_err());
157 assert!(validate_sql_identifier("../etc/passwd").is_err());
158 assert!(validate_sql_identifier("table\0name").is_err());
159 }
160}