feat(settings): configure SQL file size limits

This commit is contained in:
t8y2 authored and GitHub committed 2026-09-08 19:42:32 +08:00
1 parent 9a0e3e744c
commit 39e34d44f7
34 files changed
+554 -79

No files matched your search

+45 -1
View File
@@ -1,7 +1,21 @@
// @vitest-environment happy-dom
import { describe, expect, it } from "vitest";
import { ExternalSqlFileTooLargeError, externalSqlFileDisplayTitles, externalSqlFileOpenErrorMessage, formatSqlFileSize, isSqlFilePath, MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES, normalizeExternalSqlPath, readBrowserSqlFile } from "@/lib/sql/sqlFileOpen";
import {
ExternalSqlFileTooLargeError,
clampExternalSqlEditorMaxMbInput,
externalSqlEditorMaxBytes,
externalSqlFileDisplayTitles,
externalSqlFileOpenErrorMessage,
formatSqlFileSize,
isSqlFilePath,
MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES,
MAX_EXTERNAL_SQL_EDITOR_FILE_MB,
MIN_EXTERNAL_SQL_EDITOR_FILE_MB,
normalizeExternalSqlEditorMaxMb,
normalizeExternalSqlPath,
readBrowserSqlFile,
} from "@/lib/sql/sqlFileOpen";
describe("external SQL file paths", () => {
it("normalizes Windows separators for identity checks", () => {
@@ -41,6 +55,36 @@ describe("external SQL file editor limit", () => {
});
});
it("honors a custom browser editor limit", async () => {
const file = new Blob(["select 1;"]);
Object.defineProperty(file, "size", { value: 8 });
await expect(readBrowserSqlFile(file, 4)).rejects.toMatchObject({
name: "ExternalSqlFileTooLargeError",
sizeBytes: 8,
maxSizeBytes: 4,
});
});
it("normalizes the configurable editor size limit in MB", () => {
expect(normalizeExternalSqlEditorMaxMb(undefined)).toBe(64);
expect(normalizeExternalSqlEditorMaxMb(0)).toBe(MIN_EXTERNAL_SQL_EDITOR_FILE_MB);
expect(normalizeExternalSqlEditorMaxMb(128)).toBe(128);
expect(normalizeExternalSqlEditorMaxMb(99999)).toBe(MAX_EXTERNAL_SQL_EDITOR_FILE_MB);
expect(normalizeExternalSqlEditorMaxMb(12.6)).toBe(13);
});
it("clamps the editor size limit input", () => {
expect(clampExternalSqlEditorMaxMbInput("abc")).toBe(MIN_EXTERNAL_SQL_EDITOR_FILE_MB);
expect(clampExternalSqlEditorMaxMbInput(0)).toBe(MIN_EXTERNAL_SQL_EDITOR_FILE_MB);
expect(clampExternalSqlEditorMaxMbInput(256)).toBe(256);
expect(clampExternalSqlEditorMaxMbInput(99999)).toBe(MAX_EXTERNAL_SQL_EDITOR_FILE_MB);
});
it("converts the editor size limit to bytes", () => {
expect(externalSqlEditorMaxBytes(64)).toBe(64 * 1024 * 1024);
});
it("formats large file sizes", () => {
expect(formatSqlFileSize(64 * 1024 * 1024)).toBe("64.0 MB");
expect(formatSqlFileSize(50 * 1024 * 1024 * 1024)).toBe("50.0 GB");
+2
View File
@@ -313,6 +313,8 @@ export const saveDesktopSettings = forward("saveDesktopSettings");
export const loadMcpGlobalPolicy = forward("loadMcpGlobalPolicy");
export const saveMcpGlobalPolicy = forward("saveMcpGlobalPolicy");
export const loadMaxAgentTurns = forward("loadMaxAgentTurns");
export const loadSqlFileUploadMaxBytes = forward("loadSqlFileUploadMaxBytes");
export const saveSqlFileUploadMaxMb = forward("saveSqlFileUploadMaxMb");
export const saveMaxAgentTurns = forward("saveMaxAgentTurns");
export const loadMaxRetries = forward("loadMaxRetries");
export const saveMaxRetries = forward("saveMaxRetries");
+15 -2
View File
@@ -1848,6 +1848,19 @@ export async function loadMaxAgentTurns(): Promise<number> {
return get("/api/app-settings/max-agent-turns");
}
export async function loadSqlFileUploadMaxBytes(): Promise<number> {
return get("/api/app-settings/sql-file-upload-max-bytes");
}
export async function saveSqlFileUploadMaxMb(sqlFileUploadMaxMb: number): Promise<void> {
const res = await fetch(apiUrl("/api/app-settings/sql-file-upload-max-bytes"), {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sqlFileUploadMaxMb }),
});
if (!res.ok) throw await backendResponseError(res);
}
export async function saveMaxAgentTurns(maxAgentTurns: number): Promise<void> {
const res = await fetch(apiUrl("/api/app-settings/max-agent-turns"), {
method: "PUT",
@@ -2270,11 +2283,11 @@ export async function pendingOpenAiConfigLinks(): Promise<string[]> {
return [];
}
export async function readExternalSqlFile(_path: string): Promise<string> {
export async function readExternalSqlFile(_path: string, _maxSizeBytes?: number): Promise<string> {
throw new Error("Opening external SQL file paths is only available in the desktop app");
}
export async function readExternalSqlFileSnapshot(_path: string): Promise<import("@/lib/backend/tauri").ExternalSqlFileSnapshot> {
export async function readExternalSqlFileSnapshot(_path: string, _maxSizeBytes?: number): Promise<import("@/lib/backend/tauri").ExternalSqlFileSnapshot> {
throw new Error("Opening external SQL file paths is only available in the desktop app");
}
+12 -4
View File
@@ -717,6 +717,14 @@ export async function loadMaxAgentTurns(): Promise<number> {
return invoke("load_max_agent_turns");
}
export async function loadSqlFileUploadMaxBytes(): Promise<number> {
return 200 * 1024 * 1024;
}
export async function saveSqlFileUploadMaxMb(_sqlFileUploadMaxMb: number): Promise<void> {
// No-op on desktop: SQL files are streamed directly from disk, no server upload cap applies.
}
export async function saveMaxAgentTurns(maxAgentTurns: number): Promise<void> {
return invoke("save_max_agent_turns", { maxAgentTurns });
}
@@ -970,16 +978,16 @@ export type ExternalSqlFileStatus = { kind: "present"; sizeBytes: number; modifi
export type ExternalSqlFileWriteResult = { kind: "written"; version: ExternalSqlFileVersion } | { kind: "conflict"; currentVersion: ExternalSqlFileVersion } | { kind: "missing" };
export async function readExternalSqlFileSnapshot(path: string): Promise<ExternalSqlFileSnapshot> {
const result = await invoke<{ kind: "content"; content: string; version: ExternalSqlFileVersion } | { kind: "tooLarge"; sizeBytes: number; maxSizeBytes: number }>("read_external_sql_file", { path });
export async function readExternalSqlFileSnapshot(path: string, maxSizeBytes?: number): Promise<ExternalSqlFileSnapshot> {
const result = await invoke<{ kind: "content"; content: string; version: ExternalSqlFileVersion } | { kind: "tooLarge"; sizeBytes: number; maxSizeBytes: number }>("read_external_sql_file", { path, maxSizeBytes });
if (result.kind === "tooLarge") {
throw new ExternalSqlFileTooLargeError(result.sizeBytes, result.maxSizeBytes);
}
return { content: result.content, version: result.version };
}
export async function readExternalSqlFile(path: string): Promise<string> {
return (await readExternalSqlFileSnapshot(path)).content;
export async function readExternalSqlFile(path: string, maxSizeBytes?: number): Promise<string> {
return (await readExternalSqlFileSnapshot(path, maxSizeBytes)).content;
}
export async function inspectExternalSqlFile(path: string): Promise<ExternalSqlFileStatus> {
@@ -2,6 +2,7 @@ import type { EditorSettings } from "@/stores/settingsStore";
import { normalizeBackgroundImageSettings } from "@/lib/app/appBackgroundImage";
import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
import { normalizeQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit";
import { normalizeExternalSqlEditorMaxMb } from "@/lib/sql/sqlFileOpen";
import { normalizeCompletionTriggerMode } from "@/lib/sql/sqlCompletionTriggerPolicy";
import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates";
@@ -59,6 +60,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"tableOpenPageSize",
"queryResultMaxRowsEnabled",
"queryResultMaxRows",
"externalSqlEditorMaxMb",
"infiniteScroll",
"regexMaxMatchCount",
"autoCalculateTotalRows",
@@ -131,6 +133,7 @@ export function normalizeQueryResultMaxRowsDraft(value: unknown): number {
function normalizedDraftValue(key: EditorSettingsDraftKey, value: unknown): unknown {
if (key === "pageSize" || key === "tableOpenPageSize") return normalizeTableOpenPageSizeDraft(value);
if (key === "queryResultMaxRows") return normalizeQueryResultMaxRowsDraft(value);
if (key === "externalSqlEditorMaxMb") return normalizeExternalSqlEditorMaxMb(value);
if (key === "completionTriggerMode") return normalizeCompletionTriggerMode(value);
if (key === "redisKeyTemplates") return normalizeRedisKeyTemplates(value);
if (key === "backgroundImage") return normalizeBackgroundImageSettings(value);
@@ -1,4 +1,4 @@
export type SettingsCategory = "editor" | "formatter" | "appearance" | "navigation" | "data" | "backups" | "tunnels" | "shortcuts" | "snippets" | "sync" | "ai" | "mcp" | "security" | "about";
export type SettingsCategory = "editor" | "formatter" | "appearance" | "navigation" | "data" | "sqlFile" | "backups" | "tunnels" | "shortcuts" | "snippets" | "sync" | "ai" | "mcp" | "security" | "about";
export interface SettingsSearchContext {
isWeb: boolean;
@@ -199,6 +199,8 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "data-grid-quick-entry", category: "data", titleKey: "settings.dataGridQuickEntry", descriptionKey: "settings.dataGridQuickEntryDescription", targetId: "data" },
{ id: "data-grid-filter-view", category: "data", titleKey: "settings.dataGridFilterView", descriptionKey: "settings.dataGridFilterViewDescription", targetId: "data-grid-filter-view" },
{ id: "data-grid-flattening-multi-line", category: "data", titleKey: "settings.flatteningMultiLineText", descriptionKey: "settings.flatteningMultiLineTextDescription", targetId: "data" },
{ id: "sql-file-editor-max-mb", category: "sqlFile", titleKey: "settings.externalSqlEditorMaxMb", descriptionKey: "settings.externalSqlEditorMaxMbDescription", targetId: "sqlFile" },
{ id: "sql-file-web-upload-max-mb", category: "sqlFile", titleKey: "settings.webSqlFileUploadMaxMb", descriptionKey: "settings.webSqlFileUploadMaxMbDescription", targetId: "sqlFile", visible: (context) => context.isWeb },
{ id: "appearance-toolbar", category: "appearance", titleKey: "settings.toolbarTitle", descriptionKey: "settings.toolbarHiddenHint", targetId: "appearance" },
{ id: "appearance-exclusive-sidebar-panels", category: "appearance", titleKey: "settings.exclusiveRightSidebarPanels", descriptionKey: "settings.exclusiveRightSidebarPanelsDescription", targetId: "appearance" },
...createToolbarVisibilitySettingsSearchDefinitions(),
@@ -152,6 +152,7 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record<SettingsTransferCategoryId, readon
"tableOpenPageSize",
"queryResultMaxRowsEnabled",
"queryResultMaxRows",
"externalSqlEditorMaxMb",
"infiniteScroll",
"regexMaxMatchCount",
"autoCalculateTotalRows",
+22 -3
View File
@@ -49,6 +49,25 @@ export function externalSqlFilePaths(paths: string[]): string[] {
export const MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES = 64 * 1024 * 1024;
export const MIN_EXTERNAL_SQL_EDITOR_FILE_MB = 1;
export const MAX_EXTERNAL_SQL_EDITOR_FILE_MB = 4096;
export function normalizeExternalSqlEditorMaxMb(value: unknown): number {
const parsed = Number(value);
if (!Number.isFinite(parsed)) return MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES / (1024 * 1024);
return Math.min(MAX_EXTERNAL_SQL_EDITOR_FILE_MB, Math.max(MIN_EXTERNAL_SQL_EDITOR_FILE_MB, Math.round(parsed)));
}
export function externalSqlEditorMaxBytes(maxMb: number): number {
return Math.round(maxMb * 1024 * 1024);
}
export function clampExternalSqlEditorMaxMbInput(value: unknown): number {
const parsed = Number(value);
if (!Number.isFinite(parsed)) return MIN_EXTERNAL_SQL_EDITOR_FILE_MB;
return Math.min(MAX_EXTERNAL_SQL_EDITOR_FILE_MB, Math.max(MIN_EXTERNAL_SQL_EDITOR_FILE_MB, Math.round(parsed)));
}
export class ExternalSqlFileTooLargeError extends Error {
constructor(
readonly sizeBytes: number,
@@ -63,9 +82,9 @@ export function isExternalSqlFileTooLargeError(error: unknown): error is Externa
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));
export function readBrowserSqlFile(file: Blob, maxSizeBytes = MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES): Promise<string> {
if (file.size > maxSizeBytes) {
return Promise.reject(new ExternalSqlFileTooLargeError(file.size, maxSizeBytes));
}
return new Promise((resolve, reject) => {