Skip to main content

convergio_backup/
routes.rs

1//! HTTP routes for backup, restore, retention, and org export/import.
2
3use axum::extract::State;
4use axum::routing::{get, post};
5use axum::{Json, Router};
6use convergio_db::pool::ConnPool;
7use serde::Deserialize;
8use serde_json::{json, Value};
9use std::path::PathBuf;
10use std::sync::Arc;
11
12/// Shared state for backup routes.
13#[derive(Clone)]
14pub struct BackupState {
15    pub pool: ConnPool,
16    pub db_path: PathBuf,
17    pub backup_dir: PathBuf,
18    pub node_name: String,
19}
20
21/// Build the backup router.
22pub fn router(state: Arc<BackupState>) -> Router {
23    Router::new()
24        .route("/api/backup/snapshots", get(list_snapshots))
25        .route("/api/backup/snapshots/create", post(create_snapshot))
26        .route("/api/backup/snapshots/verify", post(verify_snapshot))
27        .route("/api/backup/restore", post(restore_snapshot))
28        .route("/api/backup/retention/rules", get(get_retention_rules))
29        .route("/api/backup/retention/rules", post(set_retention_rule))
30        .route("/api/backup/retention/purge", post(run_purge))
31        .route("/api/backup/purge-log", get(get_purge_log))
32        .route("/api/backup/export", post(export_org))
33        .route("/api/backup/import", post(import_org))
34        .with_state(state)
35}
36
37async fn list_snapshots(State(st): State<Arc<BackupState>>) -> Json<Value> {
38    match crate::snapshot::list_snapshots(&st.pool) {
39        Ok(list) => Json(json!({"ok": true, "snapshots": list})),
40        Err(e) => Json(json!({"ok": false, "error": e.to_string()})),
41    }
42}
43
44#[derive(Deserialize)]
45struct CreateSnapshotReq {
46    /// Optional label for the snapshot (reserved for future use).
47    #[serde(default)]
48    #[allow(dead_code)]
49    label: Option<String>,
50}
51
52async fn create_snapshot(
53    State(st): State<Arc<BackupState>>,
54    Json(_body): Json<CreateSnapshotReq>,
55) -> Json<Value> {
56    match crate::snapshot::create_snapshot(&st.pool, &st.db_path, &st.backup_dir, &st.node_name) {
57        Ok(rec) => Json(json!({"ok": true, "snapshot": rec})),
58        Err(e) => Json(json!({"ok": false, "error": e.to_string()})),
59    }
60}
61
62#[derive(Deserialize)]
63struct SnapshotIdReq {
64    id: String,
65}
66
67async fn verify_snapshot(
68    State(st): State<Arc<BackupState>>,
69    Json(body): Json<SnapshotIdReq>,
70) -> Json<Value> {
71    match crate::snapshot::get_snapshot(&st.pool, &body.id) {
72        Ok(rec) => match crate::snapshot::verify_snapshot(&rec) {
73            Ok(valid) => Json(json!({"ok": true, "valid": valid})),
74            Err(e) => Json(json!({"ok": false, "error": e.to_string()})),
75        },
76        Err(e) => Json(json!({"ok": false, "error": e.to_string()})),
77    }
78}
79
80async fn restore_snapshot(
81    State(st): State<Arc<BackupState>>,
82    Json(body): Json<SnapshotIdReq>,
83) -> Json<Value> {
84    match crate::restore::restore_from_snapshot(&st.pool, &body.id, &st.db_path) {
85        Ok(path) => Json(json!({"ok": true, "restored_from": path})),
86        Err(e) => Json(json!({"ok": false, "error": e.to_string()})),
87    }
88}
89
90async fn get_retention_rules(State(st): State<Arc<BackupState>>) -> Json<Value> {
91    match crate::retention::load_rules(&st.pool) {
92        Ok(rules) => Json(json!({"ok": true, "rules": rules})),
93        Err(e) => Json(json!({"ok": false, "error": e.to_string()})),
94    }
95}
96
97#[derive(Deserialize)]
98struct SetRuleReq {
99    table: String,
100    timestamp_column: Option<String>,
101    max_age_days: u32,
102    org_id: Option<String>,
103}
104
105async fn set_retention_rule(
106    State(st): State<Arc<BackupState>>,
107    Json(body): Json<SetRuleReq>,
108) -> Json<Value> {
109    let rule = crate::types::RetentionRule {
110        table: body.table,
111        timestamp_column: body.timestamp_column.unwrap_or("created_at".into()),
112        max_age_days: body.max_age_days,
113    };
114    match crate::retention::save_rule(&st.pool, &rule, body.org_id.as_deref()) {
115        Ok(()) => Json(json!({"ok": true, "rule": rule})),
116        Err(e) => Json(json!({"ok": false, "error": e.to_string()})),
117    }
118}
119
120async fn run_purge(State(st): State<Arc<BackupState>>) -> Json<Value> {
121    match crate::retention::run_auto_purge(&st.pool) {
122        Ok(events) => Json(json!({"ok": true, "events": events})),
123        Err(e) => Json(json!({"ok": false, "error": e.to_string()})),
124    }
125}
126
127async fn get_purge_log(State(st): State<Arc<BackupState>>) -> Json<Value> {
128    let conn = match st.pool.get() {
129        Ok(c) => c,
130        Err(e) => return Json(json!({"ok": false, "error": e.to_string()})),
131    };
132    let mut stmt = match conn.prepare(
133        "SELECT table_name, rows_deleted, cutoff_date, executed_at \
134         FROM backup_purge_log ORDER BY executed_at DESC LIMIT 100",
135    ) {
136        Ok(s) => s,
137        Err(e) => return Json(json!({"ok": false, "error": e.to_string()})),
138    };
139    let rows = match stmt.query_map([], |row: &rusqlite::Row<'_>| {
140        Ok(json!({
141            "table": row.get::<_, String>(0)?,
142            "rows_deleted": row.get::<_, i64>(1)?,
143            "cutoff_date": row.get::<_, String>(2)?,
144            "executed_at": row.get::<_, String>(3)?,
145        }))
146    }) {
147        Ok(r) => r,
148        Err(e) => return Json(json!({"ok": false, "error": e.to_string()})),
149    };
150    let entries: Vec<Value> = rows.filter_map(|r| r.ok()).collect();
151    Json(json!({"ok": true, "log": entries}))
152}
153
154#[derive(Deserialize)]
155struct ExportReq {
156    org_id: String,
157    org_name: String,
158}
159
160async fn export_org(
161    State(st): State<Arc<BackupState>>,
162    Json(body): Json<ExportReq>,
163) -> Json<Value> {
164    let filename = format!("org-export-{}.json", body.org_id);
165    let dest = st.backup_dir.join(&filename);
166    match crate::export::export_org_data(
167        &st.pool,
168        &body.org_id,
169        &body.org_name,
170        &st.node_name,
171        &dest,
172    ) {
173        Ok(meta) => Json(json!({"ok": true, "meta": meta, "path": dest.to_string_lossy()})),
174        Err(e) => Json(json!({"ok": false, "error": e.to_string()})),
175    }
176}
177
178#[derive(Deserialize)]
179struct ImportReq {
180    path: String,
181}
182
183async fn import_org(
184    State(st): State<Arc<BackupState>>,
185    Json(body): Json<ImportReq>,
186) -> Json<Value> {
187    let path = PathBuf::from(&body.path);
188    match crate::import::import_org_data(&st.pool, &path) {
189        Ok(result) => Json(json!({"ok": true, "result": result})),
190        Err(e) => Json(json!({"ok": false, "error": e.to_string()})),
191    }
192}