mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-23 22:48:18 +00:00
feat(sqlite): add database backup action
* feat(sqlite): add database backup action * fix(sqlite): sanitize default backup file name
This commit is contained in:
@@ -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<string | null>(() => {
|
||||
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({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1038,6 +1038,10 @@
|
||||
exportDatabase: "导出数据库",
|
||||
exportDatabaseSuccess: "已导出 {count} 张表,每表最多 {limit} 行",
|
||||
exportDatabaseFailed: "导出数据库失败:{message}",
|
||||
backupSqliteDatabase: "备份 SQLite 数据库",
|
||||
backupSqliteDatabaseInProgress: "正在备份 SQLite 数据库...",
|
||||
backupSqliteDatabaseSuccess: "SQLite 数据库已备份",
|
||||
backupSqliteDatabaseFailed: "备份 SQLite 数据库失败:{message}",
|
||||
importData: "导入数据",
|
||||
exportData: "导出数据",
|
||||
exportStructure: "导出表结构",
|
||||
|
||||
@@ -974,6 +974,10 @@
|
||||
exportDatabase: "匯出資料庫",
|
||||
exportDatabaseSuccess: "已匯出 {count} 張資料表,每張最多 {limit} 行",
|
||||
exportDatabaseFailed: "匯出資料庫失敗:{message}",
|
||||
backupSqliteDatabase: "備份 SQLite 資料庫",
|
||||
backupSqliteDatabaseInProgress: "正在備份 SQLite 資料庫...",
|
||||
backupSqliteDatabaseSuccess: "SQLite 資料庫已備份",
|
||||
backupSqliteDatabaseFailed: "備份 SQLite 資料庫失敗:{message}",
|
||||
importData: "匯入資料",
|
||||
exportData: "匯出資料",
|
||||
exportStructure: "匯出資料表結構",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -48,3 +48,29 @@ export function connectionFilePath(config: Pick<ConnectionConfig, "db_type" | "h
|
||||
export function isLocalFileDb(config: Pick<ConnectionConfig, "db_type" | "host" | "connection_string">): boolean {
|
||||
return connectionFilePath(config) !== null;
|
||||
}
|
||||
|
||||
export function isMemorySqlitePath(path: string | undefined | null): boolean {
|
||||
return (path ?? "").trim().toLowerCase() === ":memory:";
|
||||
}
|
||||
|
||||
export function sqliteBackupSourcePath(config: Pick<ConnectionConfig, "db_type" | "host">): string | null {
|
||||
if (config.db_type !== "sqlite") return null;
|
||||
const host = (config.host ?? "").trim();
|
||||
return host || null;
|
||||
}
|
||||
|
||||
export function defaultSqliteBackupFileName(config: Pick<ConnectionConfig, "host" | "name">): 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, "");
|
||||
}
|
||||
|
||||
@@ -376,6 +376,10 @@ export async function revealPathInFileManager(_path: string): Promise<void> {
|
||||
throw new Error("Reveal in file manager is only available in the desktop app.");
|
||||
}
|
||||
|
||||
export async function backupSqliteDatabase(_connectionId: string, _destinationPath: string): Promise<void> {
|
||||
throw new Error("SQLite backup is only available in the desktop app.");
|
||||
}
|
||||
|
||||
export async function syncSavedSqlDirectory(_request: SavedSqlSyncRequest): Promise<void> {
|
||||
throw new Error("SQL directory sync is only available in the desktop app.");
|
||||
}
|
||||
|
||||
@@ -999,6 +999,10 @@ export async function revealPathInFileManager(path: string): Promise<void> {
|
||||
return invoke("reveal_path_in_file_manager", { path });
|
||||
}
|
||||
|
||||
export async function backupSqliteDatabase(connectionId: string, destinationPath: string): Promise<void> {
|
||||
return invoke("backup_sqlite_database", { connectionId, destinationPath });
|
||||
}
|
||||
|
||||
export async function syncSavedSqlDirectory(request: SavedSqlSyncRequest): Promise<void> {
|
||||
return invoke("sync_saved_sql_directory", { request });
|
||||
}
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<PathBuf, String> {
|
||||
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<PathBuf, String> {
|
||||
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<bool, String> {
|
||||
Ok(canonicalize_existing_or_parent(a)? == canonicalize_existing_or_parent(b)?)
|
||||
}
|
||||
|
||||
fn canonicalize_existing_or_parent(path: &Path) -> Result<PathBuf, String> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<AppState>>,
|
||||
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<AppState>, connection_id: &str) -> Result<String, String> {
|
||||
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<AppState>, pool_key: &str) -> Result<Option<SqliteHandle>, 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),
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user