333 lines
12 KiB
Rust
333 lines
12 KiB
Rust
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use tauri::{AppHandle, Manager, State};
|
|
use tokio::sync::Mutex;
|
|
|
|
use crate::db;
|
|
use crate::db::ssh_tunnel::TunnelManager;
|
|
use crate::models::connection::{ConnectionConfig, DatabaseType};
|
|
|
|
pub enum PoolKind {
|
|
Mysql(sqlx::mysql::MySqlPool),
|
|
Postgres(sqlx::postgres::PgPool),
|
|
Sqlite(sqlx::sqlite::SqlitePool),
|
|
Redis(tokio::sync::Mutex<redis::aio::MultiplexedConnection>),
|
|
DuckDb(std::sync::Arc<std::sync::Mutex<duckdb::Connection>>),
|
|
MongoDb(mongodb::Client),
|
|
ClickHouse(db::clickhouse_driver::ChClient),
|
|
SqlServer(std::sync::Arc<tokio::sync::Mutex<db::sqlserver::SqlServerClient>>),
|
|
Oracle(std::sync::Arc<tokio::sync::Mutex<db::oracle_driver::OracleClient>>),
|
|
}
|
|
|
|
pub struct AppState {
|
|
pub connections: Mutex<HashMap<String, PoolKind>>,
|
|
pub configs: Mutex<HashMap<String, ConnectionConfig>>,
|
|
pub tunnels: TunnelManager,
|
|
}
|
|
|
|
impl AppState {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
connections: Mutex::new(HashMap::new()),
|
|
configs: Mutex::new(HashMap::new()),
|
|
tunnels: TunnelManager::new(),
|
|
}
|
|
}
|
|
|
|
pub async fn get_or_create_pool(
|
|
&self,
|
|
connection_id: &str,
|
|
database: Option<&str>,
|
|
) -> Result<String, String> {
|
|
let db_type = {
|
|
let configs = self.configs.lock().await;
|
|
configs.get(connection_id).map(|c| c.db_type.clone())
|
|
};
|
|
|
|
let is_embedded = matches!(db_type, Some(DatabaseType::Sqlite) | Some(DatabaseType::DuckDb));
|
|
if is_embedded {
|
|
return Ok(connection_id.to_string());
|
|
}
|
|
|
|
let is_single_conn = matches!(db_type, Some(DatabaseType::Oracle));
|
|
let pool_key = if is_single_conn {
|
|
connection_id.to_string()
|
|
} else {
|
|
match database {
|
|
Some(db) => format!("{connection_id}:{db}"),
|
|
None => connection_id.to_string(),
|
|
}
|
|
};
|
|
|
|
let conns = self.connections.lock().await;
|
|
if conns.contains_key(&pool_key) {
|
|
return Ok(pool_key);
|
|
}
|
|
drop(conns);
|
|
|
|
let configs = self.configs.lock().await;
|
|
let config = configs
|
|
.get(connection_id)
|
|
.ok_or("Connection config not found")?
|
|
.clone();
|
|
drop(configs);
|
|
|
|
let mut db_config = config.clone();
|
|
if let Some(db) = database {
|
|
if db_config.db_type != DatabaseType::Oracle {
|
|
db_config.database = Some(db.to_string());
|
|
}
|
|
}
|
|
|
|
let url = db_config.connection_url();
|
|
let pool = match db_config.db_type {
|
|
DatabaseType::Mysql => PoolKind::Mysql(db::mysql::connect(&url).await?),
|
|
DatabaseType::Postgres => PoolKind::Postgres(db::postgres::connect(&url).await?),
|
|
DatabaseType::Sqlite => PoolKind::Sqlite(db::sqlite::connect(&url).await?),
|
|
DatabaseType::Redis => {
|
|
let con = db::redis_driver::connect(&url).await?;
|
|
PoolKind::Redis(tokio::sync::Mutex::new(con))
|
|
}
|
|
DatabaseType::DuckDb => {
|
|
let con = duckdb::Connection::open(&db_config.host).map_err(|e| e.to_string())?;
|
|
PoolKind::DuckDb(std::sync::Arc::new(std::sync::Mutex::new(con)))
|
|
}
|
|
DatabaseType::MongoDb => {
|
|
let client = mongodb::Client::with_uri_str(&url).await.map_err(|e| e.to_string())?;
|
|
PoolKind::MongoDb(client)
|
|
}
|
|
DatabaseType::ClickHouse => {
|
|
let client = db::clickhouse_driver::ChClient::new(&url);
|
|
db::clickhouse_driver::test_connection(&client).await?;
|
|
PoolKind::ClickHouse(client)
|
|
}
|
|
DatabaseType::SqlServer => {
|
|
let client = db::sqlserver::connect(
|
|
&db_config.host, db_config.port,
|
|
&db_config.username, &db_config.password,
|
|
db_config.database.as_deref(),
|
|
).await?;
|
|
PoolKind::SqlServer(std::sync::Arc::new(tokio::sync::Mutex::new(client)))
|
|
}
|
|
DatabaseType::Oracle => {
|
|
let client = db::oracle_driver::connect(
|
|
&db_config.host, db_config.port,
|
|
db_config.database.as_deref().unwrap_or("ORCL"),
|
|
&db_config.username, &db_config.password,
|
|
).await?;
|
|
PoolKind::Oracle(std::sync::Arc::new(tokio::sync::Mutex::new(client)))
|
|
}
|
|
};
|
|
|
|
self.connections.lock().await.insert(pool_key.clone(), pool);
|
|
Ok(pool_key)
|
|
}
|
|
|
|
pub async fn reconnect_pool(
|
|
&self,
|
|
connection_id: &str,
|
|
database: Option<&str>,
|
|
) -> Result<String, String> {
|
|
let is_single_conn = {
|
|
let configs = self.configs.lock().await;
|
|
configs.get(connection_id)
|
|
.map(|c| c.db_type == DatabaseType::Oracle)
|
|
.unwrap_or(false)
|
|
};
|
|
let pool_key = if is_single_conn {
|
|
connection_id.to_string()
|
|
} else {
|
|
match database {
|
|
Some(db) => format!("{connection_id}:{db}"),
|
|
None => connection_id.to_string(),
|
|
}
|
|
};
|
|
self.connections.lock().await.remove(&pool_key);
|
|
self.get_or_create_pool(connection_id, database).await
|
|
}
|
|
}
|
|
|
|
fn connections_file(app: &AppHandle) -> Result<std::path::PathBuf, String> {
|
|
let dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
|
|
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
|
Ok(dir.join("connections.json"))
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn save_connections(
|
|
app: AppHandle,
|
|
configs: Vec<ConnectionConfig>,
|
|
) -> Result<(), String> {
|
|
let path = connections_file(&app)?;
|
|
let json = serde_json::to_string_pretty(&configs).map_err(|e| e.to_string())?;
|
|
std::fs::write(path, json).map_err(|e| e.to_string())?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn load_connections(app: AppHandle) -> Result<Vec<ConnectionConfig>, String> {
|
|
let path = connections_file(&app)?;
|
|
if !path.exists() {
|
|
return Ok(vec![]);
|
|
}
|
|
let json = std::fs::read_to_string(path).map_err(|e| e.to_string())?;
|
|
let configs: Vec<ConnectionConfig> =
|
|
serde_json::from_str(&json).map_err(|e| e.to_string())?;
|
|
Ok(configs)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn test_connection(config: ConnectionConfig) -> Result<String, String> {
|
|
let url = config.connection_url();
|
|
log::info!(
|
|
"[test_connection] db_type={:?} target={}",
|
|
config.db_type,
|
|
config.redacted_connection_url()
|
|
);
|
|
match config.db_type {
|
|
DatabaseType::Mysql => {
|
|
let pool = db::mysql::connect(&url).await?;
|
|
pool.close().await;
|
|
Ok("Connection successful".to_string())
|
|
}
|
|
DatabaseType::Postgres => {
|
|
let pool = db::postgres::connect(&url).await?;
|
|
pool.close().await;
|
|
Ok("Connection successful".to_string())
|
|
}
|
|
DatabaseType::Sqlite => {
|
|
let pool = db::sqlite::connect(&url).await?;
|
|
pool.close().await;
|
|
Ok("Connection successful".to_string())
|
|
}
|
|
DatabaseType::Redis => {
|
|
let _con = db::redis_driver::connect(&url).await?;
|
|
Ok("Connection successful".to_string())
|
|
}
|
|
DatabaseType::DuckDb => {
|
|
let _con = duckdb::Connection::open(&config.host).map_err(|e| e.to_string())?;
|
|
Ok("Connection successful".to_string())
|
|
}
|
|
DatabaseType::MongoDb => {
|
|
let client = mongodb::Client::with_uri_str(&url).await.map_err(|e| e.to_string())?;
|
|
client.list_database_names().await.map_err(|e| e.to_string())?;
|
|
Ok("Connection successful".to_string())
|
|
}
|
|
DatabaseType::ClickHouse => {
|
|
let client = db::clickhouse_driver::ChClient::new(&url);
|
|
db::clickhouse_driver::test_connection(&client).await?;
|
|
Ok("Connection successful".to_string())
|
|
}
|
|
DatabaseType::SqlServer => {
|
|
let _client = db::sqlserver::connect(
|
|
&config.host, config.port,
|
|
&config.username, &config.password,
|
|
config.database.as_deref(),
|
|
).await?;
|
|
Ok("Connection successful".to_string())
|
|
}
|
|
DatabaseType::Oracle => {
|
|
let _client = db::oracle_driver::connect(
|
|
&config.host, config.port,
|
|
config.database.as_deref().unwrap_or("ORCL"),
|
|
&config.username, &config.password,
|
|
).await?;
|
|
Ok("Connection successful".to_string())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn connect_db(
|
|
state: State<'_, Arc<AppState>>,
|
|
config: ConnectionConfig,
|
|
) -> Result<String, String> {
|
|
let id = config.id.clone();
|
|
|
|
let url = if config.ssh_enabled && !config.ssh_host.is_empty() {
|
|
let local_port = state.tunnels.start_tunnel(
|
|
&id, &config.ssh_host, config.ssh_port,
|
|
&config.ssh_user, &config.ssh_password, &config.ssh_key_path,
|
|
&config.host, config.port,
|
|
).await?;
|
|
config.connection_url_with_host("127.0.0.1", local_port)
|
|
} else {
|
|
config.connection_url()
|
|
};
|
|
|
|
let pool = match config.db_type {
|
|
DatabaseType::Mysql => PoolKind::Mysql(db::mysql::connect(&url).await?),
|
|
DatabaseType::Postgres => PoolKind::Postgres(db::postgres::connect(&url).await?),
|
|
DatabaseType::Sqlite => PoolKind::Sqlite(db::sqlite::connect(&url).await?),
|
|
DatabaseType::Redis => {
|
|
let con = db::redis_driver::connect(&url).await?;
|
|
PoolKind::Redis(tokio::sync::Mutex::new(con))
|
|
}
|
|
DatabaseType::DuckDb => {
|
|
let con = duckdb::Connection::open(&config.host).map_err(|e| e.to_string())?;
|
|
PoolKind::DuckDb(std::sync::Arc::new(std::sync::Mutex::new(con)))
|
|
}
|
|
DatabaseType::MongoDb => {
|
|
let client = mongodb::Client::with_uri_str(&url).await.map_err(|e| e.to_string())?;
|
|
PoolKind::MongoDb(client)
|
|
}
|
|
DatabaseType::ClickHouse => {
|
|
let client = db::clickhouse_driver::ChClient::new(&url);
|
|
db::clickhouse_driver::test_connection(&client).await?;
|
|
PoolKind::ClickHouse(client)
|
|
}
|
|
DatabaseType::SqlServer => {
|
|
let client = db::sqlserver::connect(
|
|
&config.host, config.port,
|
|
&config.username, &config.password,
|
|
config.database.as_deref(),
|
|
).await?;
|
|
PoolKind::SqlServer(std::sync::Arc::new(tokio::sync::Mutex::new(client))) }
|
|
DatabaseType::Oracle => {
|
|
let client = db::oracle_driver::connect(
|
|
&config.host, config.port,
|
|
config.database.as_deref().unwrap_or("ORCL"),
|
|
&config.username, &config.password,
|
|
).await?;
|
|
PoolKind::Oracle(std::sync::Arc::new(tokio::sync::Mutex::new(client)))
|
|
}
|
|
};
|
|
|
|
state.connections.lock().await.insert(id.clone(), pool);
|
|
state.configs.lock().await.insert(id.clone(), config);
|
|
|
|
Ok(id)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn disconnect_db(
|
|
state: State<'_, Arc<AppState>>,
|
|
connection_id: String,
|
|
) -> Result<(), String> {
|
|
let mut conns = state.connections.lock().await;
|
|
let keys_to_remove: Vec<String> = conns
|
|
.keys()
|
|
.filter(|k| *k == &connection_id || k.starts_with(&format!("{connection_id}:")))
|
|
.cloned()
|
|
.collect();
|
|
for key in keys_to_remove {
|
|
if let Some(pool) = conns.remove(&key) {
|
|
match pool {
|
|
PoolKind::Mysql(p) => p.close().await,
|
|
PoolKind::Postgres(p) => p.close().await,
|
|
PoolKind::Sqlite(p) => p.close().await,
|
|
PoolKind::Redis(_) => {},
|
|
PoolKind::DuckDb(_) => {},
|
|
PoolKind::MongoDb(_) => {},
|
|
PoolKind::ClickHouse(_) => {},
|
|
PoolKind::SqlServer(_) => {},
|
|
PoolKind::Oracle(_) => {},
|
|
}
|
|
}
|
|
}
|
|
drop(conns);
|
|
state.configs.lock().await.remove(&connection_id);
|
|
state.tunnels.stop_tunnel(&connection_id).await;
|
|
Ok(())
|
|
}
|