1use 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#[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
21pub 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 #[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}