fix(sql): prevent oversized SQL editor loads
This commit is contained in:
parent
fc1232fd7f
commit
f5c25510dc
|
|
@ -50,7 +50,7 @@ import { uuid } from "@/lib/common/utils";
|
|||
import { isMacOS, isWindows } from "@/lib/backend/platform";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { openQueryResultArchiveFile } from "@/lib/query/queryResultArchiveFile";
|
||||
import { sqlFileTitleFromPath } from "@/lib/sql/sqlFileOpen";
|
||||
import { externalSqlFileOpenErrorMessage, readBrowserSqlFile, sqlFileTitleFromPath } from "@/lib/sql/sqlFileOpen";
|
||||
import type { ConnectionConfig, ObjectSourceKind, QueryTab } from "@/types/database";
|
||||
import { parseConnectionDeepLink, type ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink";
|
||||
import {
|
||||
|
|
@ -1084,21 +1084,19 @@ async function openSqlFile() {
|
|||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = ".sql";
|
||||
input.onchange = () => {
|
||||
input.onchange = async () => {
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
if (typeof reader.result === "string") {
|
||||
queryStore.updateSql(tab.id, reader.result);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
try {
|
||||
queryStore.updateSql(tab.id, await readBrowserSqlFile(file));
|
||||
} catch (e: any) {
|
||||
toast(t("toolbar.sqlOpenFailed", { message: externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)) }), 5000);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
} catch (e: any) {
|
||||
toast(t("toolbar.sqlOpenFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("toolbar.sqlOpenFailed", { message: externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1131,7 +1129,7 @@ async function openSqlFilePath(path: string) {
|
|||
const database = activeTab.value?.database || (connection ? resolveDefaultDatabase(connection, []) : "");
|
||||
queryStore.openExternalSqlFile(connectionId, database, path, content);
|
||||
} catch (e: any) {
|
||||
toast(t("toolbar.sqlOpenFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("toolbar.sqlOpenFailed", { message: externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1650,7 +1648,10 @@ async function handleQuickOpenSelect(item: any) {
|
|||
const database = connection ? resolveDefaultDatabase(connection, []) : "";
|
||||
queryStore.openExternalSqlFile(connectionId, database, item.filePath, content);
|
||||
} catch (e: any) {
|
||||
toast(e?.message || String(e), 5000);
|
||||
toast(
|
||||
externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)),
|
||||
5000,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { useToast } from "@/composables/useToast";
|
|||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { resolveDefaultDatabase } from "@/lib/database/defaultDatabase";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { externalSqlFileOpenErrorMessage, formatSqlFileSize, isExternalSqlFileTooLargeError } from "@/lib/sql/sqlFileOpen";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import type { SqlFileEntry } from "@/lib/backend/api";
|
||||
import { getSqlFileFolderPaths, saveSqlFileFolderPaths, notifySqlFileFoldersChanged } from "@/lib/sqlFile/sqlFileFolders";
|
||||
|
|
@ -211,7 +212,12 @@ async function openFile(path: string) {
|
|||
const database = connection ? resolveDefaultDatabase(connection, []) : "";
|
||||
queryStore.openExternalSqlFile(connectionId, database, path, content);
|
||||
} catch (e: any) {
|
||||
toast(t("toolbar.sqlOpenFailed", { message: e?.message || String(e) }), 5000);
|
||||
if (isExternalSqlFileTooLargeError(e)) {
|
||||
executeFile(path);
|
||||
toast(t("sqlFile.largeFileExecutionOpened", { size: formatSqlFileSize(e.sizeBytes) }), 6000);
|
||||
return;
|
||||
}
|
||||
toast(t("toolbar.sqlOpenFailed", { message: externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import LightTooltip from "@/components/ui/LightTooltip.vue";
|
|||
import { useToast } from "@/composables/useToast";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { externalSqlFileOpenErrorMessage } from "@/lib/sql/sqlFileOpen";
|
||||
import { useSavedSqlStore } from "@/stores/savedSqlStore";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
|
|
@ -265,7 +266,7 @@ async function importDirectoryIntoLibrary(targetFolder?: SavedSqlFolder) {
|
|||
|
||||
toast(t("sqlLibrary.imported", { count: sqlPaths.length }), 2500);
|
||||
} catch (e: any) {
|
||||
toast(t("sqlLibrary.importFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("sqlLibrary.importFailed", { message: externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { useToast } from "@/composables/useToast";
|
|||
import * as api from "@/lib/backend/api";
|
||||
import type { ConnectionConfig } from "@/types/database";
|
||||
import { detectDatabaseFileType } from "@/lib/database/databaseFileDetection";
|
||||
import { externalSqlFileOpenErrorMessage, readBrowserSqlFile } from "@/lib/sql/sqlFileOpen";
|
||||
|
||||
function isSqlFilePath(path: string): boolean {
|
||||
return /\.sql$/i.test(path);
|
||||
|
|
@ -72,7 +73,7 @@ export function useFileDrop() {
|
|||
const content = await api.readExternalSqlFile(path);
|
||||
await openDroppedSqlFile(name, content, path);
|
||||
} catch (e: any) {
|
||||
toast(t("toolbar.sqlOpenFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("toolbar.sqlOpenFailed", { message: externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)) }), 5000);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
|
@ -108,15 +109,11 @@ export function useFileDrop() {
|
|||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
if (!isSqlFilePath(file.name)) continue;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
if (typeof reader.result === "string") {
|
||||
openDroppedSqlFile(file.name, reader.result).catch((e: any) => {
|
||||
toast(t("toolbar.sqlOpenFailed", { message: e?.message || String(e) }), 5000);
|
||||
});
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
void readBrowserSqlFile(file)
|
||||
.then((content) => openDroppedSqlFile(file.name, content))
|
||||
.catch((e: any) => {
|
||||
toast(t("toolbar.sqlOpenFailed", { message: externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)) }), 5000);
|
||||
});
|
||||
}
|
||||
});
|
||||
document.addEventListener("dragover", (event: DragEvent) => {
|
||||
|
|
|
|||
|
|
@ -3665,6 +3665,8 @@ export default {
|
|||
browse: "Browse",
|
||||
previewingLines: "Previewing {count} lines",
|
||||
previewingFirstLines: "Previewing first {count} lines",
|
||||
tooLargeForEditor: "This SQL file is {size}, exceeding the editor limit of {limit}. Use Execute SQL File to process it as a stream.",
|
||||
largeFileExecutionOpened: "This {size} SQL file is too large for the editor. Execute SQL File has been opened for streaming.",
|
||||
target: "Target",
|
||||
connection: "Connection",
|
||||
selectConnection: "Select connection",
|
||||
|
|
|
|||
|
|
@ -3471,6 +3471,8 @@ export default withEnglishFallback({
|
|||
browse: "Examinar",
|
||||
previewingLines: "Vista previa de {count} líneas",
|
||||
previewingFirstLines: "Vista previa de las primeras {count} líneas",
|
||||
tooLargeForEditor: "Este archivo SQL ocupa {size} y supera el límite de {limit} del editor. Use Ejecutar archivo SQL para procesarlo como flujo.",
|
||||
largeFileExecutionOpened: "El archivo SQL de {size} es demasiado grande para el editor. Se abrió Ejecutar archivo SQL para procesarlo como flujo.",
|
||||
target: "Destino",
|
||||
connection: "Conexión",
|
||||
selectConnection: "Seleccionar conexión",
|
||||
|
|
|
|||
|
|
@ -3469,6 +3469,8 @@ export default withEnglishFallback({
|
|||
browse: "Sfoglia",
|
||||
previewingLines: "Anteprima di {count} righe",
|
||||
previewingFirstLines: "Anteprima delle prime {count} righe",
|
||||
tooLargeForEditor: "Questo file SQL è di {size} e supera il limite di {limit} dell'editor. Usa Esegui file SQL per elaborarlo in streaming.",
|
||||
largeFileExecutionOpened: "Il file SQL di {size} è troppo grande per l'editor. È stato aperto Esegui file SQL per l'elaborazione in streaming.",
|
||||
target: "Destinazione",
|
||||
connection: "Connessione",
|
||||
selectConnection: "Seleziona connessione",
|
||||
|
|
|
|||
|
|
@ -3471,6 +3471,8 @@ export default withEnglishFallback({
|
|||
browse: "参照",
|
||||
previewingLines: "{count}行をプレビュー中",
|
||||
previewingFirstLines: "先頭{count}行をプレビュー中",
|
||||
tooLargeForEditor: "このSQLファイルは{size}あり、エディターの上限{limit}を超えています。ストリーム処理するには「SQLファイルを実行」を使用してください。",
|
||||
largeFileExecutionOpened: "{size}のSQLファイルはエディターには大きすぎるため、ストリーム処理用の「SQLファイルを実行」を開きました。",
|
||||
target: "対象",
|
||||
connection: "接続",
|
||||
selectConnection: "接続を選択",
|
||||
|
|
|
|||
|
|
@ -3471,6 +3471,8 @@ export default withEnglishFallback({
|
|||
browse: "Procurar",
|
||||
previewingLines: "Pré-visualizando {count} linhas",
|
||||
previewingFirstLines: "Pré-visualizando as primeiras {count} linhas",
|
||||
tooLargeForEditor: "Este arquivo SQL tem {size} e excede o limite de {limit} do editor. Use Executar Arquivo SQL para processá-lo por streaming.",
|
||||
largeFileExecutionOpened: "O arquivo SQL de {size} é grande demais para o editor. Executar Arquivo SQL foi aberto para processamento por streaming.",
|
||||
target: "Destino",
|
||||
connection: "Conexão",
|
||||
selectConnection: "Selecionar conexão",
|
||||
|
|
|
|||
|
|
@ -3665,6 +3665,8 @@ export default withEnglishFallback({
|
|||
browse: "浏览",
|
||||
previewingLines: "预览 {count} 行",
|
||||
previewingFirstLines: "预览前 {count} 行",
|
||||
tooLargeForEditor: "SQL 文件大小为 {size},超过编辑器 {limit} 的限制。请使用“执行 SQL 文件”以流式方式处理。",
|
||||
largeFileExecutionOpened: "该 SQL 文件大小为 {size},无法载入编辑器,已打开“执行 SQL 文件”进行流式处理。",
|
||||
target: "目标",
|
||||
connection: "连接",
|
||||
selectConnection: "选择连接",
|
||||
|
|
|
|||
|
|
@ -3142,6 +3142,8 @@ export default withEnglishFallback({
|
|||
browse: "瀏覽",
|
||||
previewingLines: "預覽 {count} 行",
|
||||
previewingFirstLines: "預覽前 {count} 行",
|
||||
tooLargeForEditor: "SQL 檔案大小為 {size},超過編輯器 {limit} 的限制。請使用「執行 SQL 檔案」以串流方式處理。",
|
||||
largeFileExecutionOpened: "此 SQL 檔案大小為 {size},無法載入編輯器,已開啟「執行 SQL 檔案」進行串流處理。",
|
||||
target: "目標",
|
||||
connection: "連線",
|
||||
selectConnection: "選擇連線",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { externalSqlFileDisplayTitles, normalizeExternalSqlPath } from "@/lib/sql/sqlFileOpen";
|
||||
import { ExternalSqlFileTooLargeError, externalSqlFileDisplayTitles, externalSqlFileOpenErrorMessage, formatSqlFileSize, MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES, normalizeExternalSqlPath, readBrowserSqlFile } from "@/lib/sql/sqlFileOpen";
|
||||
|
||||
describe("external SQL file paths", () => {
|
||||
it("normalizes Windows separators for identity checks", () => {
|
||||
|
|
@ -14,3 +16,38 @@ describe("external SQL file paths", () => {
|
|||
expect(externalSqlFileDisplayTitles(["/one/sql/create.sql", "/two/sql/create.sql"])).toEqual(["one/sql/create.sql", "two/sql/create.sql"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("external SQL file editor limit", () => {
|
||||
it("accepts the exact browser editor limit", async () => {
|
||||
const file = new Blob(["select 1;"]);
|
||||
Object.defineProperty(file, "size", { value: MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES });
|
||||
|
||||
await expect(readBrowserSqlFile(file)).resolves.toBe("select 1;");
|
||||
});
|
||||
|
||||
it("rejects browser files above the editor limit before reading", async () => {
|
||||
const file = new Blob(["select 1;"]);
|
||||
Object.defineProperty(file, "size", { value: MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES + 1 });
|
||||
|
||||
await expect(readBrowserSqlFile(file)).rejects.toMatchObject({
|
||||
name: "ExternalSqlFileTooLargeError",
|
||||
sizeBytes: MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES + 1,
|
||||
maxSizeBytes: MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES,
|
||||
});
|
||||
});
|
||||
|
||||
it("formats large file sizes", () => {
|
||||
expect(formatSqlFileSize(64 * 1024 * 1024)).toBe("64.0 MB");
|
||||
expect(formatSqlFileSize(50 * 1024 * 1024 * 1024)).toBe("50.0 GB");
|
||||
});
|
||||
|
||||
it("builds a localized actionable message for oversized files", () => {
|
||||
const message = externalSqlFileOpenErrorMessage(new ExternalSqlFileTooLargeError(50 * 1024 ** 3, 64 * 1024 ** 2), (_key, params) => `${params.size}/${params.limit}`);
|
||||
|
||||
expect(message).toBe("50.0 GB/64.0 MB");
|
||||
});
|
||||
|
||||
it("preserves ordinary backend error messages", () => {
|
||||
expect(externalSqlFileOpenErrorMessage(new Error("permission denied"), () => "unused")).toBe("permission denied");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ExternalSqlFileTooLargeError } from "@/lib/sql/sqlFileOpen";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
invoke: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@tauri-apps/api/core", () => ({
|
||||
invoke: mocks.invoke,
|
||||
}));
|
||||
|
||||
import { readExternalSqlFile } from "@/lib/backend/tauri";
|
||||
|
||||
describe("external SQL file API", () => {
|
||||
beforeEach(() => {
|
||||
mocks.invoke.mockReset();
|
||||
});
|
||||
|
||||
it("returns editor content from the structured backend response", async () => {
|
||||
mocks.invoke.mockResolvedValue({ kind: "content", content: "select 1;" });
|
||||
|
||||
await expect(readExternalSqlFile("/tmp/demo.sql")).resolves.toBe("select 1;");
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("read_external_sql_file", { path: "/tmp/demo.sql" });
|
||||
});
|
||||
|
||||
it("maps oversized responses to a typed frontend error", async () => {
|
||||
mocks.invoke.mockResolvedValue({ kind: "tooLarge", sizeBytes: 50 * 1024 ** 3, maxSizeBytes: 64 * 1024 ** 2 });
|
||||
|
||||
const error = await readExternalSqlFile("/tmp/backup.sql").catch((reason) => reason);
|
||||
|
||||
expect(error).toBeInstanceOf(ExternalSqlFileTooLargeError);
|
||||
expect(error).toMatchObject({ sizeBytes: 50 * 1024 ** 3, maxSizeBytes: 64 * 1024 ** 2 });
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { normalizeRustMongoCommand, type MongoCommand } from "@/lib/mongo/mongoShellCommand";
|
||||
import { ExternalSqlFileTooLargeError } from "@/lib/sql/sqlFileOpen";
|
||||
import type {
|
||||
ConnectionConfig,
|
||||
ConnectionTestResult,
|
||||
|
|
@ -670,7 +671,11 @@ export async function pendingOpenConnectionLinks(): Promise<string[]> {
|
|||
}
|
||||
|
||||
export async function readExternalSqlFile(path: string): Promise<string> {
|
||||
return invoke("read_external_sql_file", { path });
|
||||
const result = await invoke<{ kind: "content"; content: string } | { kind: "tooLarge"; sizeBytes: number; maxSizeBytes: number }>("read_external_sql_file", { path });
|
||||
if (result.kind === "tooLarge") {
|
||||
throw new ExternalSqlFileTooLargeError(result.sizeBytes, result.maxSizeBytes);
|
||||
}
|
||||
return result.content;
|
||||
}
|
||||
|
||||
export async function writeExternalSqlFile(path: string, content: string): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -46,3 +46,60 @@ export function externalSqlFileDisplayTitles(paths: string[]): string[] {
|
|||
export function externalSqlFilePaths(paths: string[]): string[] {
|
||||
return paths.filter(isSqlFilePath);
|
||||
}
|
||||
|
||||
export const MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES = 64 * 1024 * 1024;
|
||||
|
||||
export class ExternalSqlFileTooLargeError extends Error {
|
||||
constructor(
|
||||
readonly sizeBytes: number,
|
||||
readonly maxSizeBytes: number,
|
||||
) {
|
||||
super("SQL file is too large to open in the editor");
|
||||
this.name = "ExternalSqlFileTooLargeError";
|
||||
}
|
||||
}
|
||||
|
||||
export function isExternalSqlFileTooLargeError(error: unknown): error is ExternalSqlFileTooLargeError {
|
||||
return error instanceof ExternalSqlFileTooLargeError;
|
||||
}
|
||||
|
||||
export function readBrowserSqlFile(file: Blob): Promise<string> {
|
||||
if (file.size > MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES) {
|
||||
return Promise.reject(new ExternalSqlFileTooLargeError(file.size, MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
if (typeof reader.result === "string") resolve(reader.result);
|
||||
else reject(new Error("Failed to read SQL file as text"));
|
||||
};
|
||||
reader.onerror = () => reject(reader.error ?? new Error("Failed to read SQL file"));
|
||||
reader.onabort = () => reject(new Error("SQL file read was cancelled"));
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
export function formatSqlFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
const units = ["KB", "MB", "GB", "TB"];
|
||||
let value = bytes / 1024;
|
||||
let unit = units[0];
|
||||
for (let index = 1; index < units.length && value >= 1024; index += 1) {
|
||||
value /= 1024;
|
||||
unit = units[index];
|
||||
}
|
||||
return `${value >= 10 ? value.toFixed(1) : value.toFixed(2)} ${unit}`;
|
||||
}
|
||||
|
||||
export function externalSqlFileOpenErrorMessage(error: unknown, translate: (key: string, params: { size: string; limit: string }) => string): string {
|
||||
if (isExternalSqlFileTooLargeError(error)) {
|
||||
return translate("sqlFile.tooLargeForEditor", {
|
||||
size: formatSqlFileSize(error.sizeBytes),
|
||||
limit: formatSqlFileSize(error.maxSizeBytes),
|
||||
});
|
||||
}
|
||||
if (error instanceof Error) return error.message;
|
||||
if (error && typeof error === "object" && "message" in error && typeof error.message === "string") return error.message;
|
||||
return String(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,21 @@ use std::path::{Path, PathBuf};
|
|||
use std::sync::Mutex;
|
||||
|
||||
use dbx_core::sql::decode_sql_file_bytes;
|
||||
use serde::Serialize;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
const MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES: u64 = 64 * 1024 * 1024;
|
||||
|
||||
fn exceeds_external_sql_editor_limit(size_bytes: u64) -> bool {
|
||||
size_bytes > MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||
#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]
|
||||
pub enum ExternalSqlFileReadResult {
|
||||
Content { content: String },
|
||||
TooLarge { size_bytes: u64, max_size_bytes: u64 },
|
||||
}
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
|
||||
#[tauri::command]
|
||||
|
|
@ -13,7 +28,7 @@ pub fn pending_open_sql_files(state: tauri::State<'_, ExternalSqlOpenState>) ->
|
|||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn read_external_sql_file(path: String) -> Result<String, String> {
|
||||
pub async fn read_external_sql_file(path: String) -> Result<ExternalSqlFileReadResult, String> {
|
||||
read_external_sql_file_content_async(PathBuf::from(path)).await
|
||||
}
|
||||
|
||||
|
|
@ -90,20 +105,45 @@ pub fn is_sql_file_path(path: &Path) -> bool {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn read_external_sql_file_content(path: &Path) -> Result<String, String> {
|
||||
fn read_external_sql_file_content(path: &Path) -> Result<ExternalSqlFileReadResult, String> {
|
||||
if !is_sql_file_path(path) {
|
||||
return Err("Only .sql files can be opened this way".to_string());
|
||||
}
|
||||
let metadata = std::fs::metadata(path).map_err(|e| format!("Failed to inspect SQL file: {e}"))?;
|
||||
if exceeds_external_sql_editor_limit(metadata.len()) {
|
||||
return Ok(ExternalSqlFileReadResult::TooLarge {
|
||||
size_bytes: metadata.len(),
|
||||
max_size_bytes: MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES,
|
||||
});
|
||||
}
|
||||
let bytes = std::fs::read(path).map_err(|e| format!("Failed to read SQL file: {e}"))?;
|
||||
decode_sql_file_bytes(&bytes)
|
||||
decode_sql_file_bytes(&bytes).map(|content| ExternalSqlFileReadResult::Content { content })
|
||||
}
|
||||
|
||||
async fn read_external_sql_file_content_async(path: PathBuf) -> Result<String, String> {
|
||||
async fn read_external_sql_file_content_async(path: PathBuf) -> Result<ExternalSqlFileReadResult, String> {
|
||||
if !is_sql_file_path(&path) {
|
||||
return Err("Only .sql files can be opened this way".to_string());
|
||||
}
|
||||
let bytes = tokio::fs::read(&path).await.map_err(|e| format!("Failed to read SQL file: {e}"))?;
|
||||
decode_sql_file_bytes(&bytes)
|
||||
let file = tokio::fs::File::open(&path).await.map_err(|e| format!("Failed to read SQL file: {e}"))?;
|
||||
let metadata = file.metadata().await.map_err(|e| format!("Failed to inspect SQL file: {e}"))?;
|
||||
if exceeds_external_sql_editor_limit(metadata.len()) {
|
||||
return Ok(ExternalSqlFileReadResult::TooLarge {
|
||||
size_bytes: metadata.len(),
|
||||
max_size_bytes: MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES,
|
||||
});
|
||||
}
|
||||
let mut bytes = Vec::with_capacity(metadata.len() as usize);
|
||||
file.take(MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES + 1)
|
||||
.read_to_end(&mut bytes)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read SQL file: {e}"))?;
|
||||
if exceeds_external_sql_editor_limit(bytes.len() as u64) {
|
||||
return Ok(ExternalSqlFileReadResult::TooLarge {
|
||||
size_bytes: bytes.len() as u64,
|
||||
max_size_bytes: MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES,
|
||||
});
|
||||
}
|
||||
decode_sql_file_bytes(&bytes).map(|content| ExternalSqlFileReadResult::Content { content })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -186,7 +226,7 @@ mod tests {
|
|||
let result = read_external_sql_file_content(&path);
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
assert_eq!(result.unwrap(), "select 1;");
|
||||
assert_eq!(result.unwrap(), ExternalSqlFileReadResult::Content { content: "select 1;".to_string() });
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -197,7 +237,7 @@ mod tests {
|
|||
let result = read_external_sql_file_content(&path);
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
assert_eq!(result.unwrap(), "select '中文';");
|
||||
assert_eq!(result.unwrap(), ExternalSqlFileReadResult::Content { content: "select '中文';".to_string() });
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -211,6 +251,60 @@ mod tests {
|
|||
assert!(result.unwrap_err().contains(".sql"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_sql_editor_limit_is_inclusive() {
|
||||
assert!(!exceeds_external_sql_editor_limit(MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES));
|
||||
assert!(exceeds_external_sql_editor_limit(MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES + 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_oversized_external_sql_before_reading_content() {
|
||||
let path = std::env::temp_dir().join(format!("dbx-test-{}.sql", uuid::Uuid::new_v4()));
|
||||
let file = std::fs::File::create(&path).unwrap();
|
||||
let file_size = MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES + 1;
|
||||
file.set_len(file_size).unwrap();
|
||||
|
||||
let result = read_external_sql_file_content(&path);
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
ExternalSqlFileReadResult::TooLarge {
|
||||
size_bytes: file_size,
|
||||
max_size_bytes: MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejects_oversized_external_sql_in_async_command_path() {
|
||||
let path = std::env::temp_dir().join(format!("dbx-test-{}.sql", uuid::Uuid::new_v4()));
|
||||
let file = std::fs::File::create(&path).unwrap();
|
||||
let file_size = MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES + 1;
|
||||
file.set_len(file_size).unwrap();
|
||||
|
||||
let result = read_external_sql_file_content_async(path.clone()).await;
|
||||
|
||||
let _ = std::fs::remove_file(&path);
|
||||
assert_eq!(
|
||||
result.unwrap(),
|
||||
ExternalSqlFileReadResult::TooLarge {
|
||||
size_bytes: file_size,
|
||||
max_size_bytes: MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serializes_external_sql_file_limit_for_frontend() {
|
||||
let result = ExternalSqlFileReadResult::TooLarge { size_bytes: 100, max_size_bytes: 64 };
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(result).unwrap(),
|
||||
serde_json::json!({ "kind": "tooLarge", "sizeBytes": 100, "maxSizeBytes": 64 })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn writes_external_sql_file_content() {
|
||||
let path = std::env::temp_dir().join(format!("dbx-test-{}.sql", uuid::Uuid::new_v4()));
|
||||
|
|
|
|||
Loading…
Reference in New Issue