perf: full-stack performance and security hardening

Frontend performance:
- Debounce DataGrid search (150ms) and replace O(n) row wrapping with index array
- Replace QueryStore deep watcher with targeted field-level persistence snapshot
- Extract shared nodeConfig computed in TreeItem (eliminates 15x redundant getConfig calls)
- Clear tab result data on close to free memory
- Add LRU eviction for completion caches (50 entry cap)
- Replace savedSql version string join with numeric counter

Memory leak fixes:
- Store Tauri event unlisten handles and clean up on unmount
- Clean up DataGrid search timer on unmount

Backend security & resource management:
- Add 50MB request body size limit
- Reject file uploads >100MB and SQL files >200MB
- Clean up SSE channels after transfer/import completes
- Recover from poisoned mutex instead of panicking
- Delete temporary upload files after preview
- Validate SQL file paths to prevent directory traversal
This commit is contained in:
t8y2
2026-05-11 00:18:09 +08:00
parent 741783445d
commit f456ddd979
13 changed files with 230 additions and 87 deletions
+47 -1
View File
@@ -75,7 +75,53 @@ pub async fn execute_sql_file(
let app = state.app.clone();
let state_clone = state.clone();
let file_path = std::path::Path::new(&req.file_path);
if !file_path.is_absolute() {
return Err(AppError("File path must be absolute".to_string()));
}
if req.file_path.contains("..") {
return Err(AppError("File path must not contain '..'".to_string()));
}
tokio::spawn(async move {
match std::fs::metadata(&req.file_path) {
Ok(meta) if meta.len() > 200 * 1024 * 1024 => {
let progress = dbx_core::sql::SqlFileProgress {
execution_id: req.execution_id.clone(),
status: dbx_core::sql::SqlFileStatus::Error,
statement_index: 0,
success_count: 0,
failure_count: 0,
affected_rows: 0,
elapsed_ms: 0,
statement_summary: String::new(),
error: Some(format!("File too large: {} bytes (max {} bytes)", meta.len(), 200 * 1024 * 1024)),
};
if let Ok(json) = serde_json::to_string(&progress) {
let _ = tx.send(json);
}
return;
}
Err(e) => {
let progress = dbx_core::sql::SqlFileProgress {
execution_id: req.execution_id.clone(),
status: dbx_core::sql::SqlFileStatus::Error,
statement_index: 0,
success_count: 0,
failure_count: 0,
affected_rows: 0,
elapsed_ms: 0,
statement_summary: String::new(),
error: Some(e.to_string()),
};
if let Ok(json) = serde_json::to_string(&progress) {
let _ = tx.send(json);
}
return;
}
_ => {}
}
let file_content = match std::fs::read_to_string(&req.file_path) {
Ok(c) => c,
Err(e) => {
@@ -196,7 +242,7 @@ pub async fn execute_sql_file(
let _ = tx.send(json);
}
state_clone.sse_channels.write().await.remove(&req.execution_id);
state_clone.remove_sse_channel(&req.execution_id).await;
});
Ok(Json(serde_json::json!({ "executionId": execution_id })))