perf: optimize backend — HTTP client, DB queries, error handling, and config cleanup
Some checks failed
CI / lint-and-build (push) Failing after 2m55s
Some checks failed
CI / lint-and-build (push) Failing after 2m55s
This commit is contained in:
@@ -36,7 +36,7 @@ fn get_ai_settings_path(app: &AppHandle) -> TuskResult<std::path::PathBuf> {
|
||||
let dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| TuskError::Custom(e.to_string()))?;
|
||||
.map_err(|e| TuskError::Config(e.to_string()))?;
|
||||
fs::create_dir_all(&dir)?;
|
||||
Ok(dir.join("ai_settings.json"))
|
||||
}
|
||||
@@ -1037,7 +1037,7 @@ fn simplify_default(raw: &str) -> String {
|
||||
fn validate_select_statement(sql: &str) -> TuskResult<()> {
|
||||
let sql_upper = sql.trim().to_uppercase();
|
||||
if !sql_upper.starts_with("SELECT") {
|
||||
return Err(TuskError::Custom(
|
||||
return Err(TuskError::Validation(
|
||||
"Validation query must be a SELECT statement".to_string(),
|
||||
));
|
||||
}
|
||||
@@ -1047,7 +1047,7 @@ fn validate_select_statement(sql: &str) -> TuskResult<()> {
|
||||
fn validate_index_ddl(ddl: &str) -> TuskResult<()> {
|
||||
let ddl_upper = ddl.trim().to_uppercase();
|
||||
if !ddl_upper.starts_with("CREATE INDEX") && !ddl_upper.starts_with("DROP INDEX") {
|
||||
return Err(TuskError::Custom(
|
||||
return Err(TuskError::Validation(
|
||||
"Only CREATE INDEX and DROP INDEX statements are allowed".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ pub(crate) fn get_connections_path(app: &AppHandle) -> TuskResult<std::path::Pat
|
||||
let dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| TuskError::Custom(e.to_string()))?;
|
||||
.map_err(|e| TuskError::Config(e.to_string()))?;
|
||||
fs::create_dir_all(&dir)?;
|
||||
Ok(dir.join("connections.json"))
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ pub async fn get_table_data(
|
||||
let mut where_clause = String::new();
|
||||
if let Some(ref f) = filter {
|
||||
if !f.trim().is_empty() {
|
||||
validate_filter(f)?;
|
||||
where_clause = format!(" WHERE {}", f);
|
||||
}
|
||||
}
|
||||
@@ -285,6 +286,75 @@ pub async fn delete_rows(
|
||||
Ok(total_affected)
|
||||
}
|
||||
|
||||
/// Rejects filter strings that contain SQL statements capable of mutating data.
|
||||
/// This blocks writable CTEs and other injection attempts that could bypass
|
||||
/// SET TRANSACTION READ ONLY (which PostgreSQL does not enforce inside CTEs
|
||||
/// in all versions).
|
||||
fn validate_filter(filter: &str) -> TuskResult<()> {
|
||||
let upper = filter.to_ascii_uppercase();
|
||||
// Remove string literals to avoid false positives on keywords inside quoted values
|
||||
let sanitized = remove_string_literals(&upper);
|
||||
|
||||
const FORBIDDEN: &[&str] = &[
|
||||
"INSERT ",
|
||||
"UPDATE ",
|
||||
"DELETE ",
|
||||
"DROP ",
|
||||
"ALTER ",
|
||||
"TRUNCATE ",
|
||||
"CREATE ",
|
||||
"GRANT ",
|
||||
"REVOKE ",
|
||||
"COPY ",
|
||||
"EXECUTE ",
|
||||
"CALL ",
|
||||
];
|
||||
for kw in FORBIDDEN {
|
||||
if sanitized.contains(kw) {
|
||||
return Err(TuskError::Validation(format!(
|
||||
"Filter contains forbidden SQL keyword: {}",
|
||||
kw.trim()
|
||||
)));
|
||||
}
|
||||
}
|
||||
if sanitized.contains("INTO ") && sanitized.contains("SELECT ") {
|
||||
return Err(TuskError::Validation(
|
||||
"Filter contains forbidden SELECT INTO clause".into(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Replaces the contents of single-quoted string literals with spaces so that
|
||||
/// keyword detection does not trigger on values like `status = 'DELETE_PENDING'`.
|
||||
fn remove_string_literals(s: &str) -> String {
|
||||
let mut result = String::with_capacity(s.len());
|
||||
let mut in_quote = false;
|
||||
let mut chars = s.chars().peekable();
|
||||
while let Some(ch) = chars.next() {
|
||||
if ch == '\'' {
|
||||
if in_quote {
|
||||
// Check for escaped quote ('')
|
||||
if chars.peek() == Some(&'\'') {
|
||||
chars.next();
|
||||
result.push(' ');
|
||||
continue;
|
||||
}
|
||||
in_quote = false;
|
||||
result.push('\'');
|
||||
} else {
|
||||
in_quote = true;
|
||||
result.push('\'');
|
||||
}
|
||||
} else if in_quote {
|
||||
result.push(' ');
|
||||
} else {
|
||||
result.push(ch);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn bind_json_value<'q>(
|
||||
query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
|
||||
value: &'q Value,
|
||||
|
||||
@@ -13,7 +13,7 @@ pub async fn export_csv(
|
||||
let mut wtr = csv::Writer::from_writer(file);
|
||||
|
||||
wtr.write_record(&columns)
|
||||
.map_err(|e| TuskError::Custom(e.to_string()))?;
|
||||
.map_err(|e| TuskError::Export(e.to_string()))?;
|
||||
|
||||
for row in &rows {
|
||||
let record: Vec<String> = row
|
||||
@@ -27,10 +27,10 @@ pub async fn export_csv(
|
||||
})
|
||||
.collect();
|
||||
wtr.write_record(&record)
|
||||
.map_err(|e| TuskError::Custom(e.to_string()))?;
|
||||
.map_err(|e| TuskError::Export(e.to_string()))?;
|
||||
}
|
||||
|
||||
wtr.flush().map_err(|e| TuskError::Custom(e.to_string()))?;
|
||||
wtr.flush().map_err(|e| TuskError::Export(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ fn get_history_path(app: &AppHandle) -> TuskResult<std::path::PathBuf> {
|
||||
let dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| TuskError::Custom(e.to_string()))?;
|
||||
.map_err(|e| TuskError::Config(e.to_string()))?;
|
||||
fs::create_dir_all(&dir)?;
|
||||
Ok(dir.join("query_history.json"))
|
||||
}
|
||||
|
||||
@@ -110,11 +110,8 @@ pub async fn drop_database(
|
||||
.ok_or(TuskError::NotConnected(connection_id))?;
|
||||
|
||||
// Terminate active connections to the target database
|
||||
let terminate_sql = format!(
|
||||
"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '{}' AND pid <> pg_backend_pid()",
|
||||
name.replace('\'', "''")
|
||||
);
|
||||
sqlx::query(&terminate_sql)
|
||||
sqlx::query("SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1::name AND pid <> pg_backend_pid()")
|
||||
.bind(&name)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(TuskError::Database)?;
|
||||
|
||||
@@ -7,7 +7,7 @@ fn get_saved_queries_path(app: &AppHandle) -> TuskResult<std::path::PathBuf> {
|
||||
let dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| TuskError::Custom(e.to_string()))?;
|
||||
.map_err(|e| TuskError::Config(e.to_string()))?;
|
||||
fs::create_dir_all(&dir)?;
|
||||
Ok(dir.join("saved_queries.json"))
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ fn get_settings_path(app: &AppHandle) -> TuskResult<std::path::PathBuf> {
|
||||
let dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| TuskError::Custom(e.to_string()))?;
|
||||
.map_err(|e| TuskError::Config(e.to_string()))?;
|
||||
fs::create_dir_all(&dir)?;
|
||||
Ok(dir.join("app_settings.json"))
|
||||
}
|
||||
@@ -61,7 +61,7 @@ pub async fn save_app_settings(
|
||||
let connections_path = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| TuskError::Custom(e.to_string()))?
|
||||
.map_err(|e| TuskError::Config(e.to_string()))?
|
||||
.join("connections.json");
|
||||
|
||||
let mcp_state = state.inner().clone();
|
||||
|
||||
Reference in New Issue
Block a user