use rusqlite::params;
use super::StateStore;
use super::types::FileBackupRecord;
use crate::errors::{Result, StateError};
impl StateStore {
pub fn store_file_backup(
&self,
apply_id: i64,
file_path: &str,
state: &crate::FileState,
) -> Result<()> {
let timestamp = crate::utc_now_iso8601();
self.conn.execute(
"INSERT INTO file_backups (apply_id, file_path, content_hash, content, permissions, was_symlink, symlink_target, oversized, backed_up_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
params![
apply_id,
file_path,
state.content_hash,
state.content,
state.permissions.map(|p| p as i64),
state.is_symlink as i64,
state.symlink_target.as_ref().map(|p| p.display().to_string()),
state.oversized as i64,
timestamp,
],
)?;
Ok(())
}
pub fn get_file_backup(
&self,
apply_id: i64,
file_path: &str,
) -> Result<Option<FileBackupRecord>> {
let result = self.conn.query_row(
"SELECT id, apply_id, file_path, content_hash, content, permissions, was_symlink, symlink_target, oversized, backed_up_at
FROM file_backups WHERE apply_id = ?1 AND file_path = ?2",
params![apply_id, file_path],
|row| {
Ok(FileBackupRecord {
id: row.get(0)?,
apply_id: row.get(1)?,
file_path: row.get(2)?,
content_hash: row.get(3)?,
content: row.get(4)?,
permissions: row.get::<_, Option<i64>>(5)?.map(|p| p as u32),
was_symlink: row.get::<_, i64>(6)? != 0,
symlink_target: row.get(7)?,
oversized: row.get::<_, i64>(8)? != 0,
backed_up_at: row.get(9)?,
})
},
);
match result {
Ok(record) => Ok(Some(record)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(StateError::Database(e.to_string()).into()),
}
}
pub fn get_apply_backups(&self, apply_id: i64) -> Result<Vec<FileBackupRecord>> {
let mut stmt = self.conn.prepare(
"SELECT id, apply_id, file_path, content_hash, content, permissions, was_symlink, symlink_target, oversized, backed_up_at
FROM file_backups WHERE apply_id = ?1 ORDER BY id",
)?;
let records = stmt
.query_map(params![apply_id], |row| {
Ok(FileBackupRecord {
id: row.get(0)?,
apply_id: row.get(1)?,
file_path: row.get(2)?,
content_hash: row.get(3)?,
content: row.get(4)?,
permissions: row.get::<_, Option<i64>>(5)?.map(|p| p as u32),
was_symlink: row.get::<_, i64>(6)? != 0,
symlink_target: row.get(7)?,
oversized: row.get::<_, i64>(8)? != 0,
backed_up_at: row.get(9)?,
})
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(records)
}
pub fn latest_backup_for_path(&self, file_path: &str) -> Result<Option<FileBackupRecord>> {
let result = self.conn.query_row(
"SELECT id, apply_id, file_path, content_hash, content, permissions, was_symlink, symlink_target, oversized, backed_up_at
FROM file_backups WHERE file_path = ?1 ORDER BY id DESC LIMIT 1",
params![file_path],
|row| {
Ok(FileBackupRecord {
id: row.get(0)?,
apply_id: row.get(1)?,
file_path: row.get(2)?,
content_hash: row.get(3)?,
content: row.get(4)?,
permissions: row.get::<_, Option<i64>>(5)?.map(|p| p as u32),
was_symlink: row.get::<_, i64>(6)? != 0,
symlink_target: row.get(7)?,
oversized: row.get::<_, i64>(8)? != 0,
backed_up_at: row.get(9)?,
})
},
);
match result {
Ok(record) => Ok(Some(record)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(StateError::Database(e.to_string()).into()),
}
}
pub fn file_backups_after_apply(&self, after_apply_id: i64) -> Result<Vec<FileBackupRecord>> {
let mut stmt = self.conn.prepare(
"SELECT b.id, b.apply_id, b.file_path, b.content_hash, b.content, b.permissions,
b.was_symlink, b.symlink_target, b.oversized, b.backed_up_at
FROM file_backups b
INNER JOIN (
SELECT file_path, MIN(apply_id) AS min_apply_id
FROM file_backups
WHERE apply_id > ?1
GROUP BY file_path
) earliest ON b.file_path = earliest.file_path AND b.apply_id = earliest.min_apply_id
ORDER BY b.id",
)?;
let records = stmt
.query_map(params![after_apply_id], |row| {
Ok(FileBackupRecord {
id: row.get(0)?,
apply_id: row.get(1)?,
file_path: row.get(2)?,
content_hash: row.get(3)?,
content: row.get(4)?,
permissions: row.get::<_, Option<i64>>(5)?.map(|p| p as u32),
was_symlink: row.get::<_, i64>(6)? != 0,
symlink_target: row.get(7)?,
oversized: row.get::<_, i64>(8)? != 0,
backed_up_at: row.get(9)?,
})
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(records)
}
pub fn prune_old_backups(&self, keep_last_n: usize) -> Result<usize> {
let deleted: usize = self.conn.execute(
"DELETE FROM file_backups WHERE apply_id NOT IN (
SELECT id FROM applies ORDER BY id DESC LIMIT ?1
)",
params![keep_last_n as i64],
)?;
Ok(deleted)
}
}