Skip to main content

mixtape_tools/sqlite/maintenance/
backup.rs

1//! Backup database tool
2
3use crate::prelude::*;
4use crate::sqlite::error::SqliteToolError;
5use crate::sqlite::manager::with_connection;
6use chrono::Local;
7use std::path::PathBuf;
8
9/// Input for database backup
10#[derive(Debug, Deserialize, JsonSchema)]
11pub struct BackupDatabaseInput {
12    /// Path to the source database file. If not specified, uses the default database.
13    #[serde(default)]
14    pub source_db_path: Option<String>,
15
16    /// Destination path for the backup. If not specified, creates a timestamped
17    /// backup in the same directory as the source.
18    #[serde(default)]
19    pub backup_path: Option<PathBuf>,
20}
21
22/// Tool for creating database backups (SAFE)
23///
24/// Creates a backup copy of the database. If no backup path is specified,
25/// creates a timestamped backup in the same directory.
26pub 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            // Get source database path
44            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            // Determine backup path
53            let dest_path = match backup_path {
54                Some(p) => p,
55                None => {
56                    // Create timestamped backup in same directory
57                    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            // Use SQLite's backup API through VACUUM INTO (SQLite 3.27+)
76            // This creates a consistent backup even while the database is in use
77            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            // Get backup file size
83            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        // Create backup with explicit path
113        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}