1use rusqlite::params;
2
3use super::StateStore;
4use super::types::FileBackupRecord;
5use crate::errors::{Result, StateError};
6
7impl StateStore {
8 pub fn store_file_backup(
10 &self,
11 apply_id: i64,
12 file_path: &str,
13 state: &crate::FileState,
14 ) -> Result<()> {
15 let timestamp = crate::utc_now_iso8601();
16 self.conn.execute(
17 "INSERT INTO file_backups (apply_id, file_path, content_hash, content, permissions, was_symlink, symlink_target, oversized, backed_up_at)
18 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
19 params![
20 apply_id,
21 file_path,
22 state.content_hash,
23 state.content,
24 state.permissions.map(|p| p as i64),
25 state.is_symlink as i64,
26 state.symlink_target.as_ref().map(|p| p.display().to_string()),
27 state.oversized as i64,
28 timestamp,
29 ],
30 )?;
31 Ok(())
32 }
33
34 pub fn get_file_backup(
36 &self,
37 apply_id: i64,
38 file_path: &str,
39 ) -> Result<Option<FileBackupRecord>> {
40 let result = self.conn.query_row(
41 "SELECT id, apply_id, file_path, content_hash, content, permissions, was_symlink, symlink_target, oversized, backed_up_at
42 FROM file_backups WHERE apply_id = ?1 AND file_path = ?2",
43 params![apply_id, file_path],
44 |row| {
45 Ok(FileBackupRecord {
46 id: row.get(0)?,
47 apply_id: row.get(1)?,
48 file_path: row.get(2)?,
49 content_hash: row.get(3)?,
50 content: row.get(4)?,
51 permissions: row.get::<_, Option<i64>>(5)?.map(|p| p as u32),
52 was_symlink: row.get::<_, i64>(6)? != 0,
53 symlink_target: row.get(7)?,
54 oversized: row.get::<_, i64>(8)? != 0,
55 backed_up_at: row.get(9)?,
56 })
57 },
58 );
59
60 match result {
61 Ok(record) => Ok(Some(record)),
62 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
63 Err(e) => Err(StateError::Database(e.to_string()).into()),
64 }
65 }
66
67 pub fn get_apply_backups(&self, apply_id: i64) -> Result<Vec<FileBackupRecord>> {
69 let mut stmt = self.conn.prepare(
70 "SELECT id, apply_id, file_path, content_hash, content, permissions, was_symlink, symlink_target, oversized, backed_up_at
71 FROM file_backups WHERE apply_id = ?1 ORDER BY id",
72 )?;
73
74 let records = stmt
75 .query_map(params![apply_id], |row| {
76 Ok(FileBackupRecord {
77 id: row.get(0)?,
78 apply_id: row.get(1)?,
79 file_path: row.get(2)?,
80 content_hash: row.get(3)?,
81 content: row.get(4)?,
82 permissions: row.get::<_, Option<i64>>(5)?.map(|p| p as u32),
83 was_symlink: row.get::<_, i64>(6)? != 0,
84 symlink_target: row.get(7)?,
85 oversized: row.get::<_, i64>(8)? != 0,
86 backed_up_at: row.get(9)?,
87 })
88 })?
89 .collect::<std::result::Result<Vec<_>, _>>()?;
90
91 Ok(records)
92 }
93
94 pub fn latest_backup_for_path(&self, file_path: &str) -> Result<Option<FileBackupRecord>> {
96 let result = self.conn.query_row(
97 "SELECT id, apply_id, file_path, content_hash, content, permissions, was_symlink, symlink_target, oversized, backed_up_at
98 FROM file_backups WHERE file_path = ?1 ORDER BY id DESC LIMIT 1",
99 params![file_path],
100 |row| {
101 Ok(FileBackupRecord {
102 id: row.get(0)?,
103 apply_id: row.get(1)?,
104 file_path: row.get(2)?,
105 content_hash: row.get(3)?,
106 content: row.get(4)?,
107 permissions: row.get::<_, Option<i64>>(5)?.map(|p| p as u32),
108 was_symlink: row.get::<_, i64>(6)? != 0,
109 symlink_target: row.get(7)?,
110 oversized: row.get::<_, i64>(8)? != 0,
111 backed_up_at: row.get(9)?,
112 })
113 },
114 );
115
116 match result {
117 Ok(record) => Ok(Some(record)),
118 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
119 Err(e) => Err(StateError::Database(e.to_string()).into()),
120 }
121 }
122
123 pub fn file_backups_after_apply(&self, after_apply_id: i64) -> Result<Vec<FileBackupRecord>> {
127 let mut stmt = self.conn.prepare(
130 "SELECT b.id, b.apply_id, b.file_path, b.content_hash, b.content, b.permissions,
131 b.was_symlink, b.symlink_target, b.oversized, b.backed_up_at
132 FROM file_backups b
133 INNER JOIN (
134 SELECT file_path, MIN(apply_id) AS min_apply_id
135 FROM file_backups
136 WHERE apply_id > ?1
137 GROUP BY file_path
138 ) earliest ON b.file_path = earliest.file_path AND b.apply_id = earliest.min_apply_id
139 ORDER BY b.id",
140 )?;
141
142 let records = stmt
143 .query_map(params![after_apply_id], |row| {
144 Ok(FileBackupRecord {
145 id: row.get(0)?,
146 apply_id: row.get(1)?,
147 file_path: row.get(2)?,
148 content_hash: row.get(3)?,
149 content: row.get(4)?,
150 permissions: row.get::<_, Option<i64>>(5)?.map(|p| p as u32),
151 was_symlink: row.get::<_, i64>(6)? != 0,
152 symlink_target: row.get(7)?,
153 oversized: row.get::<_, i64>(8)? != 0,
154 backed_up_at: row.get(9)?,
155 })
156 })?
157 .collect::<std::result::Result<Vec<_>, _>>()?;
158
159 Ok(records)
160 }
161
162 pub fn prune_old_backups(&self, keep_last_n: usize) -> Result<usize> {
164 let deleted: usize = self.conn.execute(
165 "DELETE FROM file_backups WHERE apply_id NOT IN (
166 SELECT id FROM applies ORDER BY id DESC LIMIT ?1
167 )",
168 params![keep_last_n as i64],
169 )?;
170 Ok(deleted)
171 }
172}