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 })))
+7 -2
View File
@@ -34,12 +34,17 @@ pub async fn preview_import(
let file_name = field.file_name().unwrap_or("upload.csv").to_string();
let data = field.bytes().await.map_err(|e| AppError(e.to_string()))?;
if data.len() > 100 * 1024 * 1024 {
return Err(AppError(format!("File too large: {} bytes (max {} bytes)", data.len(), 100 * 1024 * 1024)));
}
let file_path = tmp_dir.join(&file_name);
std::fs::write(&file_path, &data).map_err(|e| AppError(e.to_string()))?;
let file_path_str = file_path.to_string_lossy().to_string();
let preview = table_import::preview_table_import_file_core(&file_path_str).map_err(AppError)?;
let preview = table_import::preview_table_import_file_core(&file_path_str);
let _ = std::fs::remove_file(&file_path);
let preview = preview.map_err(AppError)?;
return Ok(Json(serde_json::to_value(preview).map_err(|e| AppError(e.to_string()))?));
}
+1 -2
View File
@@ -139,8 +139,7 @@ pub async fn start_transfer(
let _ = tx.send(json);
}
// Clean up channel
state_clone.sse_channels.write().await.remove(&req.transfer_id);
state_clone.remove_sse_channel(&req.transfer_id).await;
});
Ok(Json(serde_json::json!({ "transferId": transfer_id })))