From 2d98b9fdd2405042f9103e6432ef117047cbd624 Mon Sep 17 00:00:00 2001 From: LC Date: Sat, 13 Jun 2026 18:39:12 +0800 Subject: [PATCH] feat(sqlite): add database backup action * feat(sqlite): add database backup action * fix(sqlite): sanitize default backup file name --- .../src/components/sidebar/TreeItem.vue | 48 ++++- apps/desktop/src/i18n/locales/en.ts | 4 + apps/desktop/src/i18n/locales/es.ts | 4 + apps/desktop/src/i18n/locales/it.ts | 4 + apps/desktop/src/i18n/locales/pt-BR.ts | 4 + apps/desktop/src/i18n/locales/zh-CN.ts | 4 + apps/desktop/src/i18n/locales/zh-TW.ts | 4 + apps/desktop/src/lib/api.ts | 1 + apps/desktop/src/lib/connectionFile.ts | 26 +++ apps/desktop/src/lib/http.ts | 4 + apps/desktop/src/lib/tauri.ts | 4 + crates/dbx-core/Cargo.toml | 2 +- crates/dbx-core/src/lib.rs | 1 + crates/dbx-core/src/sqlite_backup.rs | 188 ++++++++++++++++++ src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/sqlite_backup.rs | 49 +++++ src-tauri/src/lib.rs | 1 + 17 files changed, 347 insertions(+), 2 deletions(-) create mode 100644 crates/dbx-core/src/sqlite_backup.rs create mode 100644 src-tauri/src/commands/sqlite_backup.rs diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 97a62df9e9..e3d3e534fe 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -48,6 +48,7 @@ import { Clipboard, UsersRound, Lock, + HardDriveDownload, } from "@lucide/vue"; import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue"; import { useConnectionStore } from "@/stores/connectionStore"; @@ -62,7 +63,7 @@ import { uuid } from "@/lib/utils"; import { resolveDefaultDatabase } from "@/lib/defaultDatabase"; import { canTreeNodeShowExpander, treeItemPaddingLeft, usesFullWidthTreeLabel } from "@/lib/sidebarTreeItemLayout"; import { buildTableSelectSql } from "@/lib/tableSelectSql"; -import { connectionFilePath } from "@/lib/connectionFile"; +import { connectionFilePath, defaultSqliteBackupFileName, isMemorySqlitePath, sqliteBackupSourcePath } from "@/lib/connectionFile"; import { revealPathInFileManager } from "@/lib/tauri"; import { clearActiveTableReferencePayload, createTableReferencePayload, createTableReferenceDropEvent, setActiveTableReferencePayload, type QueryEditorTableReferencePayload } from "@/lib/queryEditorTableDrop"; import { editablePrimaryKeys, usesSyntheticRowIdKey } from "@/lib/tableEditing"; @@ -2138,6 +2139,44 @@ async function revealDatabaseFile() { } } +const sqliteBackupSource = computed(() => { + if (props.node.type !== "connection" || !props.node.connectionId) return null; + const config = connectionStore.getConfig(props.node.connectionId); + if (!config) return null; + return sqliteBackupSourcePath(config); +}); + +const canBackupSqliteDatabase = computed(() => { + const source = sqliteBackupSource.value; + if (!source || !props.node.connectionId) return false; + return isTauriRuntime() && (!isMemorySqlitePath(source) || connectionStore.connectedIds.has(props.node.connectionId)); +}); + +async function backupSqliteDatabase() { + const connId = props.node.connectionId; + const config = connId ? connectionStore.getConfig(connId) : undefined; + const sourcePath = sqliteBackupSource.value; + if (!connId || !config || !sourcePath) return; + + try { + const { save } = await import("@tauri-apps/plugin-dialog"); + const destinationPath = await save({ + defaultPath: defaultSqliteBackupFileName(config), + filters: [{ name: "SQLite", extensions: ["db", "sqlite", "sqlite3"] }], + }); + if (!destinationPath) return; + + toast(t("contextMenu.backupSqliteDatabaseInProgress"), 2000); + if (!isMemorySqlitePath(sourcePath)) { + await connectionStore.ensureConnected(connId); + } + await api.backupSqliteDatabase(connId, destinationPath); + toast(t("contextMenu.backupSqliteDatabaseSuccess"), 3000); + } catch (e: any) { + toast(t("contextMenu.backupSqliteDatabaseFailed", { message: e?.message || String(e) }), 5000); + } +} + async function disconnectConnection() { if (props.node.connectionId) { try { @@ -2740,6 +2779,13 @@ function treeItemMenuItems(): ContextMenuItem[] { icon: FolderOpen, }); } + if (canBackupSqliteDatabase.value) { + items.push({ + label: t("contextMenu.backupSqliteDatabase"), + action: backupSqliteDatabase, + icon: HardDriveDownload, + }); + } items.push({ label: t("contextMenu.duplicateConnection"), action: duplicateConnection, icon: CopyPlus }); items.push({ label: "", separator: true }); items.push({ diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index ca8a5bf8a6..64f5547d34 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1039,6 +1039,10 @@ exportDatabase: "Export Database", exportDatabaseSuccess: "Exported {count} tables, up to {limit} rows each", exportDatabaseFailed: "Failed to export database: {message}", + backupSqliteDatabase: "Backup SQLite Database", + backupSqliteDatabaseInProgress: "Backing up SQLite database...", + backupSqliteDatabaseSuccess: "SQLite database backed up", + backupSqliteDatabaseFailed: "Failed to backup SQLite database: {message}", importData: "Import Data", exportData: "Export Data", exportStructure: "Export Structure", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 31f695da43..50e0172d66 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -893,6 +893,10 @@ exportDatabase: "Exportar base de datos", exportDatabaseSuccess: "Se exportaron {count} tablas, hasta {limit} filas cada una", exportDatabaseFailed: "Error al exportar la base de datos: {message}", + backupSqliteDatabase: "Backup SQLite Database", + backupSqliteDatabaseInProgress: "Backing up SQLite database...", + backupSqliteDatabaseSuccess: "SQLite database backed up", + backupSqliteDatabaseFailed: "Failed to backup SQLite database: {message}", importData: "Importar datos", exportData: "Exportar datos", exportStructure: "Exportar estructura", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 04aa0f1ab0..bb394bdd5c 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -995,6 +995,10 @@ exportDatabase: "Esporta Database", exportDatabaseSuccess: "Esportate {count} tabelle, fino a {limit} righe ciascuna", exportDatabaseFailed: "Impossibile esportare il database: {message}", + backupSqliteDatabase: "Backup SQLite Database", + backupSqliteDatabaseInProgress: "Backing up SQLite database...", + backupSqliteDatabaseSuccess: "SQLite database backed up", + backupSqliteDatabaseFailed: "Failed to backup SQLite database: {message}", importData: "Importa Dati", exportData: "Esporta Dati", exportStructure: "Esporta Struttura", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 4ac8fa4060..c44dada1c1 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -995,6 +995,10 @@ exportDatabase: "Exportar Banco de Dados", exportDatabaseSuccess: "{count} tabelas exportadas, até {limit} linhas cada", exportDatabaseFailed: "Falha ao exportar banco de dados: {message}", + backupSqliteDatabase: "Backup SQLite Database", + backupSqliteDatabaseInProgress: "Backing up SQLite database...", + backupSqliteDatabaseSuccess: "SQLite database backed up", + backupSqliteDatabaseFailed: "Failed to backup SQLite database: {message}", importData: "Importar Dados", exportData: "Exportar Dados", exportStructure: "Exportar Estrutura", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index f19a5ba9ed..0af9caeafb 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1038,6 +1038,10 @@ exportDatabase: "导出数据库", exportDatabaseSuccess: "已导出 {count} 张表,每表最多 {limit} 行", exportDatabaseFailed: "导出数据库失败:{message}", + backupSqliteDatabase: "备份 SQLite 数据库", + backupSqliteDatabaseInProgress: "正在备份 SQLite 数据库...", + backupSqliteDatabaseSuccess: "SQLite 数据库已备份", + backupSqliteDatabaseFailed: "备份 SQLite 数据库失败:{message}", importData: "导入数据", exportData: "导出数据", exportStructure: "导出表结构", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index a694dc53e3..8e967d42b8 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -974,6 +974,10 @@ exportDatabase: "匯出資料庫", exportDatabaseSuccess: "已匯出 {count} 張資料表,每張最多 {limit} 行", exportDatabaseFailed: "匯出資料庫失敗:{message}", + backupSqliteDatabase: "備份 SQLite 資料庫", + backupSqliteDatabaseInProgress: "正在備份 SQLite 資料庫...", + backupSqliteDatabaseSuccess: "SQLite 資料庫已備份", + backupSqliteDatabaseFailed: "備份 SQLite 資料庫失敗:{message}", importData: "匯入資料", exportData: "匯出資料", exportStructure: "匯出資料表結構", diff --git a/apps/desktop/src/lib/api.ts b/apps/desktop/src/lib/api.ts index 50861a95c9..12417eddfd 100644 --- a/apps/desktop/src/lib/api.ts +++ b/apps/desktop/src/lib/api.ts @@ -97,6 +97,7 @@ export const deleteSavedSqlFile = forward("deleteSavedSqlFile"); export const savedSqlStorageDir = forward("savedSqlStorageDir"); export const openSavedSqlStorageDir = forward("openSavedSqlStorageDir"); export const revealPathInFileManager = forward("revealPathInFileManager"); +export const backupSqliteDatabase = forward("backupSqliteDatabase"); export const syncSavedSqlDirectory = forward("syncSavedSqlDirectory"); // Schema diff --git a/apps/desktop/src/lib/connectionFile.ts b/apps/desktop/src/lib/connectionFile.ts index 634cbb27c1..4a0f57a053 100644 --- a/apps/desktop/src/lib/connectionFile.ts +++ b/apps/desktop/src/lib/connectionFile.ts @@ -48,3 +48,29 @@ export function connectionFilePath(config: Pick): boolean { return connectionFilePath(config) !== null; } + +export function isMemorySqlitePath(path: string | undefined | null): boolean { + return (path ?? "").trim().toLowerCase() === ":memory:"; +} + +export function sqliteBackupSourcePath(config: Pick): string | null { + if (config.db_type !== "sqlite") return null; + const host = (config.host ?? "").trim(); + return host || null; +} + +export function defaultSqliteBackupFileName(config: Pick): string { + const source = (config.host || config.name || "database").trim(); + const rawFileName = isMemorySqlitePath(source) ? "memory.db" : source.split(/[\\/]/).filter(Boolean).pop() || "database.db"; + const fileName = sanitizeFileName(rawFileName) || "database.db"; + const dotIndex = fileName.lastIndexOf("."); + if (dotIndex <= 0) return `${fileName}.backup.db`; + return `${fileName.slice(0, dotIndex)}.backup${fileName.slice(dotIndex)}`; +} + +function sanitizeFileName(fileName: string): string { + return fileName + .replace(/[<>:"/\\|?*\u0000-\u001f]/g, "_") + .trim() + .replace(/[. ]+$/g, ""); +} diff --git a/apps/desktop/src/lib/http.ts b/apps/desktop/src/lib/http.ts index 997556c9ee..463e3ac9a2 100644 --- a/apps/desktop/src/lib/http.ts +++ b/apps/desktop/src/lib/http.ts @@ -376,6 +376,10 @@ export async function revealPathInFileManager(_path: string): Promise { throw new Error("Reveal in file manager is only available in the desktop app."); } +export async function backupSqliteDatabase(_connectionId: string, _destinationPath: string): Promise { + throw new Error("SQLite backup is only available in the desktop app."); +} + export async function syncSavedSqlDirectory(_request: SavedSqlSyncRequest): Promise { throw new Error("SQL directory sync is only available in the desktop app."); } diff --git a/apps/desktop/src/lib/tauri.ts b/apps/desktop/src/lib/tauri.ts index 4ad7fd53fc..8cd544e85b 100644 --- a/apps/desktop/src/lib/tauri.ts +++ b/apps/desktop/src/lib/tauri.ts @@ -999,6 +999,10 @@ export async function revealPathInFileManager(path: string): Promise { return invoke("reveal_path_in_file_manager", { path }); } +export async function backupSqliteDatabase(connectionId: string, destinationPath: string): Promise { + return invoke("backup_sqlite_database", { connectionId, destinationPath }); +} + export async function syncSavedSqlDirectory(request: SavedSqlSyncRequest): Promise { return invoke("sync_saved_sql_directory", { request }); } diff --git a/crates/dbx-core/Cargo.toml b/crates/dbx-core/Cargo.toml index d1d0c8d157..2d96ac9dac 100644 --- a/crates/dbx-core/Cargo.toml +++ b/crates/dbx-core/Cargo.toml @@ -26,7 +26,7 @@ tokio-postgres = { version = "0.7", features = ["with-chrono-0_4", "with-uuid-1" deadpool-postgres = { version = "0.14", features = ["rt_tokio_1"] } tokio-postgres-rustls = "0.13" webpki-roots = "0.26" -rusqlite = { version = "0.32", features = ["bundled", "load_extension"] } +rusqlite = { version = "0.32", features = ["bundled", "load_extension", "backup"] } mysql_async = { version = "0.36", default-features = false, features = ["default-rustls", "client_ed25519", "chrono", "rust_decimal"] } sqlparser = "0.62.0" redis = { version = "0.32.2", features = ["tokio-comp", "tls-rustls", "tls-rustls-insecure", "tokio-rustls-comp", "sentinel", "cluster-async"] } diff --git a/crates/dbx-core/src/lib.rs b/crates/dbx-core/src/lib.rs index 486a910d40..d25b057dae 100644 --- a/crates/dbx-core/src/lib.rs +++ b/crates/dbx-core/src/lib.rs @@ -42,6 +42,7 @@ pub mod sql_dialect; pub mod sql_editability; pub mod sql_file_import; pub mod sql_risk; +pub mod sqlite_backup; pub mod storage; pub mod table_export; pub mod table_import; diff --git a/crates/dbx-core/src/sqlite_backup.rs b/crates/dbx-core/src/sqlite_backup.rs new file mode 100644 index 0000000000..5d5bedb46a --- /dev/null +++ b/crates/dbx-core/src/sqlite_backup.rs @@ -0,0 +1,188 @@ +use std::path::{Path, PathBuf}; + +use rusqlite::DatabaseName; + +use crate::db::sqlite::{is_memory_database_path, SqliteHandle}; +use crate::path_utils::expand_tilde; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SqliteBackupOptions { + pub source_path: String, + pub destination_path: String, +} + +pub async fn backup_sqlite_database(pool: SqliteHandle, options: SqliteBackupOptions) -> Result<(), String> { + let destination = prepare_destination_path(&options)?; + let temp = backup_temp_path(&destination); + tokio::task::spawn_blocking(move || { + pool.with_connection(|conn| { + conn.backup(DatabaseName::Main, &temp, None).map_err(|e| format!("SQLite backup failed: {e}")) + })?; + replace_backup_file(&temp, &destination) + }) + .await + .map_err(|e| e.to_string())? +} + +fn prepare_destination_path(options: &SqliteBackupOptions) -> Result { + let destination = normalize_user_path(&options.destination_path, "Destination path is empty")?; + ensure_no_nul(&options.destination_path)?; + + if let Some(parent) = destination.parent() { + if !parent.as_os_str().is_empty() { + std::fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + } + if destination.is_dir() { + return Err("Backup destination must be a file".to_string()); + } + + let source = options.source_path.trim(); + if !is_memory_database_path(source) && !source.is_empty() { + ensure_no_nul(source)?; + let source = normalize_user_path(source, "Source path is empty")?; + if paths_match(&source, &destination)? { + return Err("Backup destination must be different from the source database file".to_string()); + } + } + + Ok(destination) +} + +fn backup_temp_path(destination: &Path) -> PathBuf { + let file_name = destination.file_name().and_then(|name| name.to_str()).unwrap_or("backup.db"); + destination.with_file_name(format!("{file_name}.tmp-{}", uuid::Uuid::new_v4())) +} + +fn replace_backup_file(temp: &Path, destination: &Path) -> Result<(), String> { + if destination.exists() { + let backup = existing_backup_path(destination); + std::fs::rename(destination, &backup) + .map_err(|e| format!("Failed to back up existing destination file: {e}"))?; + match std::fs::rename(temp, destination) { + Ok(()) => { + std::fs::remove_file(&backup).ok(); + Ok(()) + } + Err(err) => { + let _ = std::fs::rename(&backup, destination); + Err(format!("Failed to replace backup destination: {err}")) + } + } + } else { + std::fs::rename(temp, destination).map_err(|e| format!("Failed to move backup into place: {e}")) + } +} + +fn existing_backup_path(destination: &Path) -> PathBuf { + let file_name = destination.file_name().and_then(|name| name.to_str()).unwrap_or("backup.db"); + destination.with_file_name(format!("{file_name}.backup-{}", uuid::Uuid::new_v4())) +} + +fn normalize_user_path(path: &str, empty_message: &str) -> Result { + let trimmed = path.trim(); + if trimmed.is_empty() { + return Err(empty_message.to_string()); + } + Ok(PathBuf::from(expand_tilde(trimmed))) +} + +fn ensure_no_nul(path: &str) -> Result<(), String> { + if path.contains('\0') { + Err("Path contains an invalid NUL byte".to_string()) + } else { + Ok(()) + } +} + +fn paths_match(a: &Path, b: &Path) -> Result { + Ok(canonicalize_existing_or_parent(a)? == canonicalize_existing_or_parent(b)?) +} + +fn canonicalize_existing_or_parent(path: &Path) -> Result { + if let Ok(canonical) = std::fs::canonicalize(path) { + return Ok(canonical); + } + let parent = path.parent().filter(|parent| !parent.as_os_str().is_empty()).unwrap_or_else(|| Path::new(".")); + let parent = std::fs::canonicalize(parent).map_err(|e| e.to_string())?; + Ok(parent.join(path.file_name().ok_or_else(|| "Path is missing a file name".to_string())?)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::sqlite::{connect_path, execute_query}; + + #[tokio::test] + async fn backup_sqlite_database_copies_memory_database() { + let dir = std::env::temp_dir().join(format!("dbx-sqlite-backup-{}", uuid::Uuid::new_v4())); + std::fs::create_dir_all(&dir).expect("create temp dir"); + let destination = dir.join("backup.db"); + let pool = connect_path(":memory:").await.expect("connect memory sqlite"); + execute_query(&pool, "CREATE TABLE users (name TEXT); INSERT INTO users VALUES ('Ada');") + .await + .expect("seed memory database"); + + backup_sqlite_database( + pool, + SqliteBackupOptions { + source_path: ":memory:".to_string(), + destination_path: destination.to_string_lossy().to_string(), + }, + ) + .await + .expect("backup memory database"); + + let backup = connect_path(destination.to_string_lossy().as_ref()).await.expect("open backup"); + let result = execute_query(&backup, "SELECT name FROM users").await.expect("query backup"); + assert_eq!(result.rows[0][0], serde_json::json!("Ada")); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn prepare_destination_rejects_same_file() { + let dir = std::env::temp_dir().join(format!("dbx-sqlite-backup-{}", uuid::Uuid::new_v4())); + std::fs::create_dir_all(&dir).expect("create temp dir"); + let source = dir.join("source.db"); + std::fs::write(&source, "").expect("create source"); + + let result = prepare_destination_path(&SqliteBackupOptions { + source_path: source.to_string_lossy().to_string(), + destination_path: source.to_string_lossy().to_string(), + }); + + assert!(result.expect_err("same file should be rejected").contains("different")); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn prepare_destination_creates_missing_parent_dir() { + let dir = std::env::temp_dir().join(format!("dbx-sqlite-backup-{}", uuid::Uuid::new_v4())); + let destination = dir.join("nested").join("backup.db"); + + let result = prepare_destination_path(&SqliteBackupOptions { + source_path: ":memory:".to_string(), + destination_path: destination.to_string_lossy().to_string(), + }) + .expect("prepare destination"); + + assert_eq!(result, destination); + assert!(destination.parent().expect("parent").is_dir()); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn replace_backup_file_restores_existing_destination_when_replace_fails() { + let dir = std::env::temp_dir().join(format!("dbx-sqlite-backup-{}", uuid::Uuid::new_v4())); + std::fs::create_dir_all(&dir).expect("create temp dir"); + let destination = dir.join("backup.db"); + let missing_temp = dir.join("missing.db"); + std::fs::write(&destination, "existing").expect("create destination"); + + let result = replace_backup_file(&missing_temp, &destination); + + assert!(result.expect_err("replace should fail").contains("replace")); + assert_eq!(std::fs::read_to_string(&destination).expect("read destination"), "existing"); + let _ = std::fs::remove_dir_all(&dir); + } +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 63ddd6ad68..215c9b3a93 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -27,6 +27,7 @@ pub mod schema; pub mod schema_cache; pub mod schema_diff; pub mod sql_file; +pub mod sqlite_backup; pub mod system_fonts; pub mod tab_runtime_cache; pub mod table_export; diff --git a/src-tauri/src/commands/sqlite_backup.rs b/src-tauri/src/commands/sqlite_backup.rs new file mode 100644 index 0000000000..e7012c4910 --- /dev/null +++ b/src-tauri/src/commands/sqlite_backup.rs @@ -0,0 +1,49 @@ +use std::sync::Arc; + +use tauri::State; + +use dbx_core::connection::{AppState, PoolKind}; +use dbx_core::db::sqlite::{is_memory_database_path, SqliteHandle}; +use dbx_core::models::connection::DatabaseType; +use dbx_core::sqlite_backup::{backup_sqlite_database as backup_sqlite_database_core, SqliteBackupOptions}; + +#[tauri::command] +pub async fn backup_sqlite_database( + state: State<'_, Arc>, + connection_id: String, + destination_path: String, +) -> Result<(), String> { + let source_path = sqlite_source_path(&state, &connection_id).await?; + let pool = if is_memory_database_path(&source_path) { + existing_sqlite_pool(&state, &connection_id) + .await? + .ok_or_else(|| "Open the in-memory SQLite connection before backing it up".to_string())? + } else { + let pool_key = state.get_or_create_pool(&connection_id, None).await?; + existing_sqlite_pool(&state, &pool_key).await?.ok_or_else(|| "Connection is not open".to_string())? + }; + + backup_sqlite_database_core(pool, SqliteBackupOptions { source_path, destination_path }).await +} + +async fn sqlite_source_path(state: &Arc, connection_id: &str) -> Result { + let configs = state.configs.read().await; + let config = configs.get(connection_id).ok_or_else(|| format!("Connection config not found: {connection_id}"))?; + if config.db_type != DatabaseType::Sqlite { + return Err("SQLite backup is only available for SQLite connections".to_string()); + } + let source = config.host.trim(); + if source.is_empty() { + return Err("SQLite database path is empty".to_string()); + } + Ok(source.to_string()) +} + +async fn existing_sqlite_pool(state: &Arc, pool_key: &str) -> Result, String> { + let connections = state.connections.read().await; + match connections.get(pool_key) { + Some(PoolKind::Sqlite(pool)) => Ok(Some(pool.clone())), + Some(_) => Err("SQLite backup is only available for SQLite connections".to_string()), + None => Ok(None), + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c137ff6913..d07513c1da 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -497,6 +497,7 @@ pub fn run() { commands::saved_sql::open_saved_sql_storage_dir, commands::saved_sql::sync_saved_sql_directory, commands::fs_open::reveal_path_in_file_manager, + commands::sqlite_backup::backup_sqlite_database, commands::mongo_cmd::mongo_list_databases, commands::mongo_cmd::mongo_list_collections, commands::mongo_cmd::document_find_documents,