fix(sqlite): validate ssh backup and query errors
This commit is contained in:
1 parent
e6f8b2187a
commit
9190554e50
58 files changed
+2815
-149
No files matched your search
@@ -384,6 +384,27 @@ mod tests {
|
||||
assert!(error.contains("encrypted connections"), "{error}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sqlite_ssh_config_does_not_open_the_remote_path_as_a_local_file() {
|
||||
let mut config = sqlite_config(std::path::Path::new("/remote/data/app.db"), "");
|
||||
config.transport_layers = serde_json::from_value(serde_json::json!([{
|
||||
"type": "ssh",
|
||||
"id": "hop-1",
|
||||
"enabled": true,
|
||||
"host": "203.0.113.10",
|
||||
"port": 22,
|
||||
"user": "testuser"
|
||||
}]))
|
||||
.expect("ssh layer");
|
||||
|
||||
let error = match connect_sqlite_from_config(&config).await {
|
||||
Ok(_) => panic!("SSH SQLite must not open a remote path on the local filesystem"),
|
||||
Err(error) => error,
|
||||
};
|
||||
assert!(error.contains("SSH") || error.contains("Desktop") || error.contains("worker"), "{error}");
|
||||
assert!(!error.contains("File does not exist"), "{error}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn saving_memory_sqlite_attachments_keeps_the_live_pool_intact() {
|
||||
let dir = std::env::temp_dir().join(format!("dbx-tauri-sqlite-memory-{}", uuid::Uuid::new_v4()));
|
||||
@@ -869,7 +890,7 @@ pub async fn save_connections(state: State<'_, Arc<AppState>>, configs: Vec<Conn
|
||||
|
||||
async fn save_connection_configs(state: &AppState, configs: &[ConnectionConfig]) -> Result<(), String> {
|
||||
for config in configs {
|
||||
if config.db_type == DatabaseType::Sqlite {
|
||||
if config.db_type == DatabaseType::Sqlite && !db::sqlite_worker::sqlite_ssh_worker_requested(config) {
|
||||
db::sqlite::validate_persistent_attachments(
|
||||
&config.host,
|
||||
&config.password,
|
||||
@@ -1003,7 +1024,34 @@ fn sqlite_extension_specs_from_config(config: &ConnectionConfig) -> Vec<db::sqli
|
||||
.collect()
|
||||
}
|
||||
|
||||
// Thin wrapper for the test suite below: production callers all go through
|
||||
// `connect_sqlite_from_config_with_state` with a real AppState.
|
||||
#[cfg(test)]
|
||||
async fn connect_sqlite_from_config(config: &ConnectionConfig) -> Result<db::sqlite::SqliteHandle, String> {
|
||||
connect_sqlite_from_config_with_state(None, config.id.as_str(), config).await
|
||||
}
|
||||
|
||||
async fn connect_sqlite_from_config_with_state(
|
||||
state: Option<&AppState>,
|
||||
connection_id: &str,
|
||||
config: &ConnectionConfig,
|
||||
) -> Result<db::sqlite::SqliteHandle, String> {
|
||||
if db::sqlite_worker::sqlite_ssh_worker_requested(config) {
|
||||
let state =
|
||||
state.ok_or_else(|| "Remote SQLite over SSH is only available in the DBX Desktop app".to_string())?;
|
||||
let transport_layers = state.resolved_transport_layers(config).await?;
|
||||
let worker = db::sqlite_worker::connect_sqlite_worker(
|
||||
&state.tunnels,
|
||||
&state.agent_manager,
|
||||
state.storage.data_dir(),
|
||||
connection_id,
|
||||
config,
|
||||
&transport_layers,
|
||||
)
|
||||
.await?;
|
||||
return Ok(db::sqlite::SqliteHandle::from_worker(worker));
|
||||
}
|
||||
|
||||
let sqlite_path = expand_tilde(&config.host);
|
||||
db::sqlite::validate_persistent_attachments(&sqlite_path, &config.password, !config.attached_databases.is_empty())?;
|
||||
let pool = db::sqlite::connect_path_with_cipher_key_and_extensions(
|
||||
@@ -1180,10 +1228,15 @@ async fn test_connection_with_info_inner(
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
DatabaseType::Sqlite => match connect_sqlite_from_config(&config).await {
|
||||
Ok(_) => Ok("Connection successful".to_string()),
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
DatabaseType::Sqlite => {
|
||||
match connect_sqlite_from_config_with_state(Some(state.as_ref()), connection_id, &config).await {
|
||||
Ok(pool) => {
|
||||
pool.shutdown().await;
|
||||
Ok("Connection successful".to_string())
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
DatabaseType::Redis => {
|
||||
// Keep the result inside the outer lifecycle so temporary transports
|
||||
// are reset after both successful and failed Redis tests.
|
||||
@@ -1573,7 +1626,7 @@ pub async fn connect_db(
|
||||
client_attempt: Option<u64>,
|
||||
) -> Result<String, String> {
|
||||
let config = config.canonicalized();
|
||||
if config.db_type == DatabaseType::Sqlite {
|
||||
if config.db_type == DatabaseType::Sqlite && !db::sqlite_worker::sqlite_ssh_worker_requested(&config) {
|
||||
db::sqlite::validate_persistent_attachments(
|
||||
&config.host,
|
||||
&config.password,
|
||||
@@ -1627,7 +1680,9 @@ pub async fn connect_db(
|
||||
| DatabaseType::Kwdb
|
||||
| DatabaseType::Questdb
|
||||
| DatabaseType::OpenGauss => PoolKind::Postgres(db::postgres::connect(&url, connect_timeout).await?),
|
||||
DatabaseType::Sqlite => PoolKind::Sqlite(connect_sqlite_from_config(&db_config).await?),
|
||||
DatabaseType::Sqlite => PoolKind::Sqlite(
|
||||
connect_sqlite_from_config_with_state(Some(state.inner().as_ref()), &id, &db_config).await?,
|
||||
),
|
||||
DatabaseType::Redis => {
|
||||
let con = if db_config.uses_redis_cluster() {
|
||||
PoolKind::Redis(db::redis_driver::RedisConnection::Cluster(
|
||||
@@ -1969,7 +2024,9 @@ pub async fn connection_final_proxy_port(
|
||||
if !runtime_config.has_effective_transport_layers() {
|
||||
return Err("Connection has no configured transport layers".to_string());
|
||||
}
|
||||
if runtime_config.db_type == DatabaseType::Sqlite {
|
||||
if runtime_config.db_type == DatabaseType::Sqlite
|
||||
&& !db::sqlite_worker::sqlite_ssh_worker_requested(&runtime_config)
|
||||
{
|
||||
db::sqlite::validate_persistent_attachments(
|
||||
&runtime_config.host,
|
||||
&runtime_config.password,
|
||||
|
||||
@@ -5,7 +5,10 @@ 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};
|
||||
use dbx_core::sqlite_backup::{
|
||||
backup_sqlite_database as backup_sqlite_database_core, restore_sqlite_database as restore_sqlite_database_core,
|
||||
SqliteBackupOptions,
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn backup_sqlite_database(
|
||||
@@ -26,6 +29,17 @@ pub async fn backup_sqlite_database(
|
||||
backup_sqlite_database_core(pool, SqliteBackupOptions { source_path, destination_path }).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn restore_sqlite_database(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
connection_id: String,
|
||||
source_path: String,
|
||||
) -> Result<(), String> {
|
||||
let pool_key = state.get_or_create_pool(&connection_id, None).await?;
|
||||
let pool = existing_sqlite_pool(&state, &pool_key).await?.ok_or_else(|| "Connection is not open".to_string())?;
|
||||
restore_sqlite_database_core(pool, &source_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}"))?;
|
||||
|
||||
@@ -1605,6 +1605,7 @@ pub fn run() {
|
||||
} else {
|
||||
AppState::new_with_plugin_dir_and_app_version(storage, plugin_dir, env!("CARGO_PKG_VERSION"))
|
||||
};
|
||||
dbx_core::db::sqlite_worker::enable_sqlite_ssh_runtime(env!("CARGO_PKG_VERSION"));
|
||||
state.set_duckdb_worker_process_isolation_enabled(desktop_settings.duckdb_worker_process_isolation);
|
||||
state.set_duckdb_worker_max_processes(desktop_settings.duckdb_worker_max_processes);
|
||||
let oidc_app_handle = app.handle().clone();
|
||||
@@ -2174,6 +2175,7 @@ pub fn run() {
|
||||
commands::fs_open::is_sqlite_database_file,
|
||||
commands::fs_open::delete_database_backup_files,
|
||||
commands::sqlite_backup::backup_sqlite_database,
|
||||
commands::sqlite_backup::restore_sqlite_database,
|
||||
commands::mongo_cmd::mongo_list_databases,
|
||||
commands::mongo_cmd::mongo_list_collections,
|
||||
commands::vector_cmd::vector_collection_detail,
|
||||
|
||||
Reference in new issue
Block a user