Skip to main content

aft/db/
backups.rs

1use rusqlite::{params, Connection, OptionalExtension, Row};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct BackupRow {
5    pub backup_id: String,
6    pub harness: String,
7    pub session_id: String,
8    pub project_key: String,
9    pub op_id: Option<String>,
10    pub order: u128,
11    pub file_path: String,
12    pub path_hash: String,
13    pub backup_path: Option<String>,
14    pub kind: String,
15    pub description: String,
16    pub created_at: i64,
17    pub is_tombstone: bool,
18    pub restore_meta: Option<String>,
19}
20
21pub fn upsert_backup(conn: &Connection, row: &BackupRow) -> rusqlite::Result<()> {
22    delete_backup_for_order(
23        conn,
24        &row.harness,
25        &row.session_id,
26        &row.path_hash,
27        row.order,
28    )?;
29    insert_backup(conn, row)
30}
31
32pub fn insert_backup(conn: &Connection, row: &BackupRow) -> rusqlite::Result<()> {
33    let order_blob = row.order.to_be_bytes();
34    conn.execute(
35        "INSERT INTO backups (
36            backup_id, harness, session_id, project_key, op_id, order_blob, file_path,
37            path_hash, backup_path, kind, description, created_at, is_tombstone, restore_meta
38         ) VALUES (
39            ?1, ?2, ?3, ?4, ?5, ?6, ?7,
40            ?8, ?9, ?10, ?11, ?12, ?13, ?14
41         )",
42        params![
43            row.backup_id,
44            row.harness,
45            row.session_id,
46            row.project_key,
47            row.op_id,
48            &order_blob[..],
49            row.file_path,
50            row.path_hash,
51            row.backup_path,
52            row.kind,
53            row.description,
54            row.created_at,
55            row.is_tombstone,
56            row.restore_meta,
57        ],
58    )?;
59
60    Ok(())
61}
62
63pub fn delete_backup_for_order(
64    conn: &Connection,
65    harness: &str,
66    session_id: &str,
67    path_hash: &str,
68    order: u128,
69) -> rusqlite::Result<usize> {
70    let order_blob = order.to_be_bytes();
71    conn.execute(
72        "DELETE FROM backups
73         WHERE harness = ?1 AND session_id = ?2 AND path_hash = ?3 AND order_blob = ?4",
74        params![harness, session_id, path_hash, &order_blob[..]],
75    )
76}
77
78pub fn get_latest_backup(
79    conn: &Connection,
80    harness: &str,
81    session_id: &str,
82    path_hash: &str,
83) -> rusqlite::Result<Option<BackupRow>> {
84    conn.query_row(
85        "SELECT backup_id, harness, session_id, project_key, op_id, order_blob, file_path,
86                path_hash, backup_path, kind, description, created_at, is_tombstone, restore_meta
87         FROM backups
88         WHERE harness = ?1 AND session_id = ?2 AND path_hash = ?3
89         ORDER BY order_blob DESC
90         LIMIT 1",
91        params![harness, session_id, path_hash],
92        map_backup_row,
93    )
94    .optional()
95}
96
97pub fn list_backups(
98    conn: &Connection,
99    harness: &str,
100    session_id: &str,
101    path_hash: &str,
102) -> rusqlite::Result<Vec<BackupRow>> {
103    let mut stmt = conn.prepare(
104        "SELECT backup_id, harness, session_id, project_key, op_id, order_blob, file_path,
105                path_hash, backup_path, kind, description, created_at, is_tombstone, restore_meta
106         FROM backups
107         WHERE harness = ?1 AND session_id = ?2 AND path_hash = ?3
108         ORDER BY order_blob ASC",
109    )?;
110
111    let rows = stmt
112        .query_map(params![harness, session_id, path_hash], map_backup_row)?
113        .collect();
114    rows
115}
116
117pub fn list_backups_by_op(
118    conn: &Connection,
119    harness: &str,
120    session_id: &str,
121    op_id: &str,
122) -> rusqlite::Result<Vec<BackupRow>> {
123    let mut stmt = conn.prepare(
124        "SELECT backup_id, harness, session_id, project_key, op_id, order_blob, file_path,
125                path_hash, backup_path, kind, description, created_at, is_tombstone, restore_meta
126         FROM backups
127         WHERE harness = ?1 AND session_id = ?2 AND op_id = ?3
128         ORDER BY file_path ASC, order_blob ASC",
129    )?;
130
131    let rows = stmt
132        .query_map(params![harness, session_id, op_id], map_backup_row)?
133        .collect();
134    rows
135}
136
137pub fn get_latest_operation_backup(
138    conn: &Connection,
139    harness: &str,
140    session_id: &str,
141) -> rusqlite::Result<Option<BackupRow>> {
142    conn.query_row(
143        "SELECT backup_id, harness, session_id, project_key, op_id, order_blob, file_path,
144                path_hash, backup_path, kind, description, created_at, is_tombstone, restore_meta
145         FROM backups
146         WHERE harness = ?1 AND session_id = ?2 AND op_id IS NOT NULL
147         ORDER BY order_blob DESC
148         LIMIT 1",
149        params![harness, session_id],
150        map_backup_row,
151    )
152    .optional()
153}
154
155pub fn delete_backups_for_path(
156    conn: &Connection,
157    harness: &str,
158    session_id: &str,
159    path_hash: &str,
160) -> rusqlite::Result<usize> {
161    conn.execute(
162        "DELETE FROM backups WHERE harness = ?1 AND session_id = ?2 AND path_hash = ?3",
163        params![harness, session_id, path_hash],
164    )
165}
166
167fn map_backup_row(row: &Row<'_>) -> rusqlite::Result<BackupRow> {
168    let order_blob: Vec<u8> = row.get(5)?;
169    let order = order_from_blob(&order_blob).unwrap_or_default();
170    Ok(BackupRow {
171        backup_id: row.get::<_, Option<String>>(0)?.unwrap_or_default(),
172        harness: row.get(1)?,
173        session_id: row.get(2)?,
174        project_key: row.get(3)?,
175        op_id: row.get(4)?,
176        order,
177        file_path: row.get(6)?,
178        path_hash: row.get(7)?,
179        backup_path: row.get(8)?,
180        kind: row.get(9)?,
181        description: row.get::<_, Option<String>>(10)?.unwrap_or_default(),
182        created_at: row.get(11)?,
183        is_tombstone: row.get::<_, i64>(12)? != 0,
184        restore_meta: row.get(13)?,
185    })
186}
187
188fn order_from_blob(blob: &[u8]) -> Option<u128> {
189    let bytes: [u8; 16] = blob.try_into().ok()?;
190    Some(u128::from_be_bytes(bytes))
191}