Skip to main content

heldar_kernel/models/
backup.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use sqlx::types::Json;
5use sqlx::FromRow;
6
7/// Config keys that hold a secret. Masked in [`BackupDestinationView`] (and preserved across an
8/// update when the client round-trips the `***` placeholder back).
9pub const BACKUP_SECRET_KEYS: &[&str] = &["pass", "password", "secret_key", "secret"];
10
11/// A backup transfer target. `config` is a kind-specific JSON blob (credentials live here and are
12/// never serialized raw — use [`BackupDestinationView`]). Not `Serialize` for exactly that reason.
13#[derive(Debug, Clone, FromRow)]
14pub struct BackupDestination {
15    pub id: String,
16    pub name: String,
17    /// `local` | `sftp` | `ftp` | `s3`.
18    pub kind: String,
19    pub config: Json<Value>,
20    pub enabled: bool,
21    pub created_at: DateTime<Utc>,
22    pub updated_at: DateTime<Utc>,
23}
24
25/// Client-facing destination: secret config values are replaced with `***`.
26#[derive(Debug, Clone, Serialize)]
27pub struct BackupDestinationView {
28    pub id: String,
29    pub name: String,
30    pub kind: String,
31    /// The config blob with any secret values masked to `***`.
32    pub config: Value,
33    /// Whether at least one secret credential is configured (so the UI can show "set" without the value).
34    pub has_credentials: bool,
35    pub enabled: bool,
36    pub created_at: DateTime<Utc>,
37    pub updated_at: DateTime<Utc>,
38}
39
40/// Mask the secret values in a config blob, returning the masked blob and whether any secret was set.
41pub fn mask_backup_config(mut config: Value) -> (Value, bool) {
42    let mut has_credentials = false;
43    if let Some(obj) = config.as_object_mut() {
44        for key in BACKUP_SECRET_KEYS {
45            if let Some(v) = obj.get_mut(*key) {
46                if v.as_str().map(|s| !s.is_empty()).unwrap_or(false) {
47                    has_credentials = true;
48                    *v = Value::String("***".to_string());
49                }
50            }
51        }
52    }
53    (config, has_credentials)
54}
55
56impl From<BackupDestination> for BackupDestinationView {
57    fn from(d: BackupDestination) -> Self {
58        let (config, has_credentials) = mask_backup_config(d.config.0);
59        BackupDestinationView {
60            id: d.id,
61            name: d.name,
62            kind: d.kind,
63            config,
64            has_credentials,
65            enabled: d.enabled,
66            created_at: d.created_at,
67            updated_at: d.updated_at,
68        }
69    }
70}
71
72#[derive(Debug, Deserialize)]
73pub struct BackupDestinationCreate {
74    pub name: String,
75    /// `local` | `sftp` | `ftp` | `s3`.
76    pub kind: String,
77    pub config: Option<Value>,
78    pub enabled: Option<bool>,
79}
80
81#[derive(Debug, Deserialize, Default)]
82pub struct BackupDestinationUpdate {
83    pub name: Option<String>,
84    pub kind: Option<String>,
85    pub config: Option<Value>,
86    pub enabled: Option<bool>,
87}
88
89/// Result of POST /api/v1/backup/destinations/{id}/test (a connectivity / writability probe).
90#[derive(Debug, Clone, Serialize)]
91pub struct BackupTestResult {
92    pub ok: bool,
93    pub error: Option<String>,
94    pub latency_ms: i64,
95}
96
97/// A scheduled backup policy: ship a camera selection's recent footage to a destination on an interval.
98#[derive(Debug, Clone, Serialize, FromRow)]
99pub struct BackupPolicy {
100    pub id: String,
101    pub name: String,
102    pub destination_id: String,
103    /// JSON array of camera ids; empty array means all cameras.
104    pub camera_ids: Json<Value>,
105    pub incident_lock_only: bool,
106    pub schedule_interval_s: i64,
107    pub lookback_hours: i64,
108    pub last_run_at: Option<DateTime<Utc>>,
109    pub last_job_id: Option<String>,
110    pub enabled: bool,
111    pub created_at: DateTime<Utc>,
112    pub updated_at: DateTime<Utc>,
113}
114
115#[derive(Debug, Deserialize)]
116pub struct BackupPolicyCreate {
117    pub name: String,
118    pub destination_id: String,
119    pub camera_ids: Option<Value>,
120    pub incident_lock_only: Option<bool>,
121    pub schedule_interval_s: Option<i64>,
122    pub lookback_hours: Option<i64>,
123    pub enabled: Option<bool>,
124}
125
126#[derive(Debug, Deserialize, Default)]
127pub struct BackupPolicyUpdate {
128    pub name: Option<String>,
129    pub destination_id: Option<String>,
130    pub camera_ids: Option<Value>,
131    pub incident_lock_only: Option<bool>,
132    pub schedule_interval_s: Option<i64>,
133    pub lookback_hours: Option<i64>,
134    pub enabled: Option<bool>,
135}
136
137/// A single backup run (policy-scheduled, manually triggered, or an on-demand archive export).
138#[derive(Debug, Clone, Serialize, FromRow)]
139pub struct BackupJob {
140    pub id: String,
141    pub policy_id: Option<String>,
142    pub destination_id: Option<String>,
143    /// `policy` | `on_demand_archive`.
144    pub kind: String,
145    pub camera_ids: Json<Value>,
146    pub from_time: Option<DateTime<Utc>>,
147    pub to_time: Option<DateTime<Utc>>,
148    pub incident_lock_only: bool,
149    /// `pending` | `running` | `completed` | `error`.
150    pub status: String,
151    pub files_total: i64,
152    pub files_copied: i64,
153    pub bytes_copied: i64,
154    pub error: Option<String>,
155    /// Filesystem path of the produced artifact (archive .zip), if any.
156    pub output_path: Option<String>,
157    /// Browser-fetchable URL of the produced artifact (under /media/archives/...), if any.
158    pub output_url: Option<String>,
159    pub started_at: Option<DateTime<Utc>>,
160    pub finished_at: Option<DateTime<Utc>>,
161    pub created_at: DateTime<Utc>,
162}
163
164/// Request body for POST /api/v1/archive/export — zip a selection of recorded footage on demand.
165#[derive(Debug, Deserialize)]
166pub struct ArchiveExportRequest {
167    /// Camera ids to include; empty/omitted means all cameras.
168    #[serde(default)]
169    pub camera_ids: Vec<String>,
170    pub from: Option<String>,
171    pub to: Option<String>,
172    pub incident_lock_only: Option<bool>,
173    /// Trim each segment to the [from, to] window (re-mux with -c copy); requires both bounds.
174    pub trim: Option<bool>,
175}