mixtape_tools/sqlite/maintenance/
backup.rs1use crate::prelude::*;
4use crate::sqlite::error::SqliteToolError;
5use crate::sqlite::manager::with_connection;
6use chrono::Local;
7use std::path::PathBuf;
8
9#[derive(Debug, Deserialize, JsonSchema)]
11pub struct BackupDatabaseInput {
12 #[serde(default)]
14 pub source_db_path: Option<String>,
15
16 #[serde(default)]
19 pub backup_path: Option<PathBuf>,
20}
21
22pub struct BackupDatabaseTool;
27
28impl Tool for BackupDatabaseTool {
29 type Input = BackupDatabaseInput;
30
31 fn name(&self) -> &str {
32 "sqlite_backup"
33 }
34
35 fn description(&self) -> &str {
36 "Create a backup copy of the database. Optionally specify a destination path, or let it create a timestamped backup automatically."
37 }
38
39 async fn execute(&self, input: Self::Input) -> Result<ToolResult, ToolError> {
40 let backup_path = input.backup_path;
41
42 let (path, size) = with_connection(input.source_db_path, move |conn| {
43 let source_db_path: String = conn
45 .query_row("PRAGMA database_list", [], |row| row.get(2))
46 .map_err(|_| {
47 SqliteToolError::QueryError("Could not get database path".to_string())
48 })?;
49
50 let source_db_pathbuf = PathBuf::from(&source_db_path);
51
52 let dest_path = match backup_path {
54 Some(p) => p,
55 None => {
56 let timestamp = Local::now().format("%Y%m%d_%H%M%S");
58 let stem = source_db_pathbuf
59 .file_stem()
60 .and_then(|s| s.to_str())
61 .unwrap_or("database");
62 let ext = source_db_pathbuf
63 .extension()
64 .and_then(|s| s.to_str())
65 .unwrap_or("db");
66
67 let backup_name = format!("{}_{}.{}", stem, timestamp, ext);
68 source_db_pathbuf
69 .parent()
70 .map(|p| p.join(backup_name))
71 .unwrap_or_else(|| PathBuf::from(format!("backup_{}.db", timestamp)))
72 }
73 };
74
75 let backup_sql = format!("VACUUM INTO '{}'", dest_path.to_string_lossy());
78
79 conn.execute(&backup_sql, [])
80 .map_err(|e| SqliteToolError::QueryError(format!("Backup failed: {}", e)))?;
81
82 let size = std::fs::metadata(&dest_path).map(|m| m.len()).unwrap_or(0);
84
85 Ok((dest_path.to_string_lossy().to_string(), size))
86 })
87 .await?;
88
89 let response = serde_json::json!({
90 "status": "success",
91 "backup_path": path,
92 "size_bytes": size,
93 "message": format!("Database backed up to: {}", path)
94 });
95 Ok(ToolResult::Json(response))
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use crate::sqlite::test_utils::{unwrap_json, TestDatabase};
103
104 #[tokio::test]
105 async fn test_backup_database() {
106 let db = TestDatabase::with_schema(
107 "CREATE TABLE test (id INTEGER);
108 INSERT INTO test VALUES (1);",
109 )
110 .await;
111
112 let backup_path = db.path().parent().unwrap().join("backup.db");
114 let tool = BackupDatabaseTool;
115 let input = BackupDatabaseInput {
116 source_db_path: Some(db.key()),
117 backup_path: Some(backup_path.clone()),
118 };
119
120 let result = tool.execute(input).await.unwrap();
121 let json = unwrap_json(result);
122
123 assert_eq!(json["status"].as_str().unwrap(), "success");
124 assert!(backup_path.exists());
125 }
126
127 #[tokio::test]
128 async fn test_backup_auto_path() {
129 let db = TestDatabase::new().await;
130
131 let tool = BackupDatabaseTool;
132 let input = BackupDatabaseInput {
133 source_db_path: Some(db.key()),
134 backup_path: None,
135 };
136
137 let result = tool.execute(input).await.unwrap();
138 let json = unwrap_json(result);
139
140 assert_eq!(json["status"].as_str().unwrap(), "success");
141 let backup_path = json["backup_path"].as_str().unwrap();
142 assert!(backup_path.contains("test_"));
143 assert!(std::path::Path::new(backup_path).exists());
144 }
145
146 #[test]
147 fn test_tool_metadata() {
148 let tool = BackupDatabaseTool;
149 assert_eq!(tool.name(), "sqlite_backup");
150 assert!(!tool.description().is_empty());
151 }
152}