mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
feat(settings): SQL File Size tab with configurable web upload limit
Group SQL file size settings into a dedicated settings tab: - Editor limit (from previous commit) moves from the Data tab - New web upload limit for the web Execute SQL File streaming path, persisted in storage (default 200 MB, clamped 1-4096 MB), editable from the web settings dialog and exposed via GET/PUT /api/app-settings/sql-file-upload-max-bytes - The /sql-file/preview body limit becomes a hard upper bound; the effective per-request limit is enforced in the handlers - Web dialog fetches the effective limit for its client-side pre-check - i18n across all locales; settings search + transfer category maps
This commit is contained in:
@@ -138,6 +138,8 @@ import {
|
||||
saveMaxAgentTurns,
|
||||
loadMaxRetries,
|
||||
saveMaxRetries,
|
||||
loadSqlFileUploadMaxBytes,
|
||||
saveSqlFileUploadMaxMb,
|
||||
saveWebdavSyncSecretsPreference,
|
||||
saveWebdavSavedPassword,
|
||||
saveSnippetSavedToken,
|
||||
@@ -2389,6 +2391,7 @@ const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[
|
||||
{ value: "formatter", label: t("settings.sqlFormatterTab") },
|
||||
{ value: "navigation", label: t("settings.navigationTab") },
|
||||
{ value: "data", label: t("settings.dataTab") },
|
||||
{ value: "sqlFile", label: t("settings.sqlFileSizeTab") },
|
||||
...(isWeb ? [] : [{ value: "backups" as const, label: t("databaseBackup.title") }]),
|
||||
{ value: "tunnels", label: t("settings.tunnelsTab") },
|
||||
{ value: "shortcuts", label: t("settings.shortcutsTab") },
|
||||
@@ -2399,7 +2402,7 @@ const settingsCategoryNav = computed<{ value: SettingsCategory; label: string }[
|
||||
...(isWeb ? [{ value: "security" as const, label: t("settings.securityTab") }] : []),
|
||||
{ value: "about", label: t("settings.aboutTab") },
|
||||
]);
|
||||
const settingsTabsWithApplyFooter = new Set<SettingsCategory>(["editor", "formatter", "appearance", "navigation", "data", "shortcuts", "snippets"]);
|
||||
const settingsTabsWithApplyFooter = new Set<SettingsCategory>(["editor", "formatter", "appearance", "navigation", "data", "sqlFile", "shortcuts", "snippets"]);
|
||||
|
||||
function hasSettingsApplyFooter(value: SettingsCategory): boolean {
|
||||
return settingsTabsWithApplyFooter.has(value);
|
||||
@@ -3847,6 +3850,7 @@ watch(activeSettingsTab, async (tab) => {
|
||||
await promptTemplateStore.ensureLoaded();
|
||||
editGlobalInstructions.value = promptTemplateStore.globalInstructions;
|
||||
}
|
||||
if (tab === "sqlFile" && isWeb) void loadWebSqlFileUploadMaxMbSetting();
|
||||
if (tab === "about" && !appSupportInfo.value) void refreshAppSupportInfo();
|
||||
if (tab === "appearance") {
|
||||
checkLayoutDescTruncation();
|
||||
@@ -4154,6 +4158,52 @@ function normalizeMaxRetries(value: number | undefined): number {
|
||||
return Math.min(10, Math.max(0, rounded));
|
||||
}
|
||||
|
||||
const editWebSqlFileUploadMaxMb = ref<number | undefined>(undefined);
|
||||
const webSqlFileUploadMaxMbSaving = ref(false);
|
||||
const webSqlFileUploadMaxMbLoaded = ref(false);
|
||||
const webSqlFileUploadMaxMbLoading = ref(false);
|
||||
const webSqlFileUploadMaxMbLoadError = ref("");
|
||||
|
||||
async function loadWebSqlFileUploadMaxMbSetting() {
|
||||
if (webSqlFileUploadMaxMbLoaded.value || webSqlFileUploadMaxMbLoading.value) return;
|
||||
webSqlFileUploadMaxMbLoading.value = true;
|
||||
webSqlFileUploadMaxMbLoadError.value = "";
|
||||
try {
|
||||
const bytes = await loadSqlFileUploadMaxBytes();
|
||||
editWebSqlFileUploadMaxMb.value = Math.round(bytes / (1024 * 1024));
|
||||
webSqlFileUploadMaxMbLoaded.value = true;
|
||||
} catch (e: any) {
|
||||
webSqlFileUploadMaxMbLoadError.value = e?.message || String(e);
|
||||
toast(webSqlFileUploadMaxMbLoadError.value, 5000);
|
||||
} finally {
|
||||
webSqlFileUploadMaxMbLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeWebSqlFileUploadMaxMb(value: number | undefined): number {
|
||||
const rounded = typeof value === "number" && Number.isFinite(value) ? Math.round(value) : 200;
|
||||
return Math.min(MAX_EXTERNAL_SQL_EDITOR_FILE_MB, Math.max(MIN_EXTERNAL_SQL_EDITOR_FILE_MB, rounded));
|
||||
}
|
||||
|
||||
function webSqlFileUploadMaxMbOutOfRange(value: number | undefined): boolean {
|
||||
return typeof value === "number" && (value < MIN_EXTERNAL_SQL_EDITOR_FILE_MB || value > MAX_EXTERNAL_SQL_EDITOR_FILE_MB);
|
||||
}
|
||||
|
||||
async function saveWebSqlFileUploadMaxMbSetting() {
|
||||
if (!webSqlFileUploadMaxMbLoaded.value) return;
|
||||
const clamped = normalizeWebSqlFileUploadMaxMb(editWebSqlFileUploadMaxMb.value);
|
||||
webSqlFileUploadMaxMbSaving.value = true;
|
||||
try {
|
||||
await saveSqlFileUploadMaxMb(clamped);
|
||||
editWebSqlFileUploadMaxMb.value = clamped;
|
||||
toast(t("settings.sqlFileUploadMaxMbSaved"));
|
||||
} catch (e: any) {
|
||||
toast(e?.message || String(e), 5000);
|
||||
} finally {
|
||||
webSqlFileUploadMaxMbSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// AI Config Delete Confirmation
|
||||
const aiDeleteConfirmOpen = ref(false);
|
||||
const aiDeleteConfigId = ref<string | null>(null);
|
||||
@@ -6971,26 +7021,6 @@ onUnmounted(() => {
|
||||
<Switch id="query-result-max-rows-enabled" v-model="editQueryResultMaxRowsEnabled" :aria-label="t('settings.queryResultMaxRowsEnabled')" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="external-sql-editor-max-mb">
|
||||
{{ t("settings.externalSqlEditorMaxMb") }}
|
||||
</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.externalSqlEditorMaxMbDescription", { min: MIN_EXTERNAL_SQL_EDITOR_FILE_MB, max: MAX_EXTERNAL_SQL_EDITOR_FILE_MB }) }}
|
||||
</p>
|
||||
</div>
|
||||
<Input
|
||||
id="external-sql-editor-max-mb"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
class="h-7 w-[130px] px-2 text-left text-xs tabular-nums"
|
||||
:min="MIN_EXTERNAL_SQL_EDITOR_FILE_MB"
|
||||
:max="MAX_EXTERNAL_SQL_EDITOR_FILE_MB"
|
||||
:model-value="editExternalSqlEditorMaxMb"
|
||||
@input="updateExternalSqlEditorMaxMbInput"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="infinite-scroll">
|
||||
@@ -7468,6 +7498,55 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeSettingsTab === 'sqlFile'" data-settings-search-id="sqlFile" :class="['flex flex-col gap-5 py-2', settingsSearchTargetClass('sqlFile')]">
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="external-sql-editor-max-mb">
|
||||
{{ t("settings.externalSqlEditorMaxMb") }}
|
||||
</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.externalSqlEditorMaxMbDescription", { min: MIN_EXTERNAL_SQL_EDITOR_FILE_MB, max: MAX_EXTERNAL_SQL_EDITOR_FILE_MB }) }}
|
||||
</p>
|
||||
</div>
|
||||
<Input
|
||||
id="external-sql-editor-max-mb"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
class="h-7 w-[130px] px-2 text-left text-xs tabular-nums"
|
||||
:min="MIN_EXTERNAL_SQL_EDITOR_FILE_MB"
|
||||
:max="MAX_EXTERNAL_SQL_EDITOR_FILE_MB"
|
||||
:model-value="editExternalSqlEditorMaxMb"
|
||||
@input="updateExternalSqlEditorMaxMbInput"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isWeb" class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="web-sql-file-upload-max-mb">
|
||||
{{ t("settings.webSqlFileUploadMaxMb") }}
|
||||
</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.webSqlFileUploadMaxMbDescription", { min: MIN_EXTERNAL_SQL_EDITOR_FILE_MB, max: MAX_EXTERNAL_SQL_EDITOR_FILE_MB }) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<Input
|
||||
id="web-sql-file-upload-max-mb"
|
||||
v-model.number="editWebSqlFileUploadMaxMb"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
class="h-7 w-[130px] px-2 text-left text-xs tabular-nums"
|
||||
:min="MIN_EXTERNAL_SQL_EDITOR_FILE_MB"
|
||||
:max="MAX_EXTERNAL_SQL_EDITOR_FILE_MB"
|
||||
:disabled="!webSqlFileUploadMaxMbLoaded || webSqlFileUploadMaxMbLoading"
|
||||
:aria-invalid="webSqlFileUploadMaxMbOutOfRange(editWebSqlFileUploadMaxMb)"
|
||||
/>
|
||||
<Button type="button" size="sm" variant="outline" :disabled="!webSqlFileUploadMaxMbLoaded || webSqlFileUploadMaxMbSaving || webSqlFileUploadMaxMbOutOfRange(editWebSqlFileUploadMaxMb)" @click="saveWebSqlFileUploadMaxMbSetting">
|
||||
{{ t("settings.save") }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeSettingsTab === 'shortcuts'" data-settings-search-id="shortcuts" :class="['flex flex-col gap-2 py-2', settingsSearchTargetClass('shortcuts')]">
|
||||
<div class="relative">
|
||||
<Search class="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { uuid } from "@/lib/common/utils";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
|
||||
@@ -97,7 +97,21 @@ const terminalError = ref("");
|
||||
const activeExecutionTask = ref<ExportTask | null>(null);
|
||||
const failureDetailsExpanded = ref(false);
|
||||
const refreshedTarget = ref(false);
|
||||
const MAX_WEB_SQL_FILE_BYTES = 200 * 1024 * 1024;
|
||||
const DEFAULT_WEB_SQL_FILE_BYTES = 200 * 1024 * 1024;
|
||||
const webSqlFileUploadMaxBytes = ref(DEFAULT_WEB_SQL_FILE_BYTES);
|
||||
|
||||
// The web server caps SQL file uploads via DBX_SQL_FILE_UPLOAD_MAX_MB; fetch the
|
||||
// effective limit once so the client-side pre-check matches the server rule.
|
||||
onMounted(async () => {
|
||||
if (isDesktopRuntime) return;
|
||||
try {
|
||||
const { loadSqlFileUploadMaxBytes } = await import("@/lib/backend/http");
|
||||
const value = await loadSqlFileUploadMaxBytes();
|
||||
if (Number.isFinite(value) && value > 0) webSqlFileUploadMaxBytes.value = value;
|
||||
} catch {
|
||||
// Fall back to the built-in default; the server still rejects oversized uploads.
|
||||
}
|
||||
});
|
||||
|
||||
// Per-file results accumulated from backend file-boundary events during
|
||||
// multi-file execution. Populated only when previews.length > 1.
|
||||
@@ -285,8 +299,9 @@ async function previewSelectedSqlFile(fileOrPath: string | File) {
|
||||
return previewSqlFile(fileOrPath as string);
|
||||
}
|
||||
const file = fileOrPath as File;
|
||||
if (file.size > MAX_WEB_SQL_FILE_BYTES) {
|
||||
throw new Error(`File too large: ${file.size} bytes (max ${MAX_WEB_SQL_FILE_BYTES} bytes)`);
|
||||
const uploadLimit = webSqlFileUploadMaxBytes.value;
|
||||
if (file.size > uploadLimit) {
|
||||
throw new Error(`File too large: ${file.size} bytes (max ${uploadLimit} bytes)`);
|
||||
}
|
||||
const { previewSqlFile: previewWebSqlFile } = await import("@/lib/backend/http");
|
||||
return previewWebSqlFile(file);
|
||||
|
||||
@@ -6540,6 +6540,7 @@ export default {
|
||||
appearanceTab: "Appearance",
|
||||
navigationTab: "Navigation",
|
||||
dataTab: "Data",
|
||||
sqlFileSizeTab: "SQL File Size",
|
||||
tunnelsTab: "Tunnels",
|
||||
tunnelsDescription: "Reusable SSH / proxy / HTTP tunnel configurations. Configure once, then select the profile from a connection's tunnel tab; edits here apply to every connection using the profile.",
|
||||
tunnelsEmpty: "No tunnel profiles yet. Create one here, then select it in a connection's tunnel settings.",
|
||||
@@ -6743,6 +6744,9 @@ export default {
|
||||
queryResultMaxRowsTooSmall: "Must be at least the query page size ({pageSize}).",
|
||||
externalSqlEditorMaxMb: "Maximum SQL editor file size",
|
||||
externalSqlEditorMaxMbDescription: "SQL files larger than this are routed to the streaming Execute SQL File dialog instead of the editor ({min}–{max} MB).",
|
||||
webSqlFileUploadMaxMb: "Maximum web SQL file upload size",
|
||||
webSqlFileUploadMaxMbDescription: "Largest SQL file the web server accepts for streaming execution ({min}–{max} MB).",
|
||||
sqlFileUploadMaxMbSaved: "Web SQL file upload size saved.",
|
||||
showColumnCommentsInHeader: "Show column comments under names",
|
||||
showColumnCommentsInHeaderDescription: "Display table column comments directly below grid column names.",
|
||||
showColumnTypesInHeader: "Show column types under names",
|
||||
|
||||
@@ -6131,6 +6131,7 @@ export default withEnglishFallback({
|
||||
appearanceTab: "Apariencia",
|
||||
navigationTab: "Navegación",
|
||||
dataTab: "Datos",
|
||||
sqlFileSizeTab: "Tamaño de archivo SQL",
|
||||
tunnelsTab: "Túneles",
|
||||
tunnelsDescription: "Configuraciones reutilizables de túneles SSH / proxy / HTTP. Configura una vez y selecciona el perfil desde la pestaña de túnel de una conexión; los cambios aquí se aplican a todas las conexiones que usan el perfil.",
|
||||
tunnelsEmpty: "Aún no hay perfiles de túnel. Crea uno aquí y selecciónalo en la configuración de túnel de una conexión.",
|
||||
@@ -6323,6 +6324,9 @@ export default withEnglishFallback({
|
||||
queryResultMaxRowsTooSmall: "Debe ser al menos el tamaño de página de consulta ({pageSize}).",
|
||||
externalSqlEditorMaxMb: "Tamaño máximo de archivo del editor SQL",
|
||||
externalSqlEditorMaxMbDescription: "Los archivos SQL mayores que esto se envían al diálogo de ejecución por streaming en lugar del editor ({min}–{max} MB).",
|
||||
webSqlFileUploadMaxMb: "Tamaño máximo de carga de archivo SQL web",
|
||||
webSqlFileUploadMaxMbDescription: "Archivo SQL más grande que el servidor web acepta para ejecución en streaming ({min}–{max} MB).",
|
||||
sqlFileUploadMaxMbSaved: "Tamaño de carga de archivo SQL web guardado.",
|
||||
showColumnCommentsInHeader: "Mostrar comentarios bajo los nombres",
|
||||
showColumnCommentsInHeaderDescription: "Muestra los comentarios de columnas directamente debajo del nombre de la columna.",
|
||||
showColumnTypesInHeader: "Mostrar tipos de columna bajo los nombres",
|
||||
|
||||
@@ -6131,6 +6131,7 @@ export default withEnglishFallback({
|
||||
appearanceTab: "Aspetto",
|
||||
navigationTab: "Navigazione",
|
||||
dataTab: "Dati",
|
||||
sqlFileSizeTab: "Dimensione file SQL",
|
||||
tunnelsTab: "Tunnel",
|
||||
tunnelsDescription: "Configurazioni riutilizzabili di tunnel SSH / proxy / HTTP. Configura una volta, poi seleziona il profilo dalla scheda tunnel di una connessione; le modifiche qui si applicano a tutte le connessioni che usano il profilo.",
|
||||
tunnelsEmpty: "Nessun profilo tunnel. Creane uno qui, poi selezionalo nelle impostazioni tunnel di una connessione.",
|
||||
@@ -6323,6 +6324,9 @@ export default withEnglishFallback({
|
||||
queryResultMaxRowsTooSmall: "Deve essere almeno pari alle righe per pagina della query ({pageSize}).",
|
||||
externalSqlEditorMaxMb: "Dimensione massima file dell'editor SQL",
|
||||
externalSqlEditorMaxMbDescription: "I file SQL più grandi vengono inviati alla finestra di esecuzione in streaming invece che all'editor ({min}–{max} MB).",
|
||||
webSqlFileUploadMaxMb: "Dimensione massima di caricamento file SQL web",
|
||||
webSqlFileUploadMaxMbDescription: "File SQL più grande che il server web accetta per l'esecuzione in streaming ({min}–{max} MB).",
|
||||
sqlFileUploadMaxMbSaved: "Dimensione di caricamento file SQL web salvata.",
|
||||
showColumnCommentsInHeader: "Mostra i commenti delle colonne sotto i nomi",
|
||||
showColumnCommentsInHeaderDescription: "Visualizza i commenti delle colonne della tabella direttamente sotto i nomi delle colonne nella griglia.",
|
||||
showColumnTypesInHeader: "Mostra i tipi di colonna sotto i nomi",
|
||||
|
||||
@@ -6169,6 +6169,7 @@ export default withEnglishFallback({
|
||||
appearanceTab: "外観",
|
||||
navigationTab: "ナビゲーション",
|
||||
dataTab: "データ",
|
||||
sqlFileSizeTab: "SQLファイルサイズ",
|
||||
tunnelsTab: "トンネル",
|
||||
tunnelsDescription: "再利用可能な SSH / プロキシ / HTTP トンネル設定です。一度設定すれば、接続のトンネルタブからプロファイルを選択するだけで使えます。ここでの編集は、このプロファイルを使用するすべての接続に反映されます。",
|
||||
tunnelsEmpty: "トンネルプロファイルがまだありません。ここで作成し、接続のトンネル設定で選択してください。",
|
||||
@@ -6357,6 +6358,9 @@ export default withEnglishFallback({
|
||||
queryResultMaxRowsTooSmall: "クエリのページ行数({pageSize})以上にしてください。",
|
||||
externalSqlEditorMaxMb: "SQLエディターの最大ファイルサイズ",
|
||||
externalSqlEditorMaxMbDescription: "このサイズを超えるSQLファイルはエディターではなくストリーミング実行ダイアログに回されます({min}~{max} MB)。",
|
||||
webSqlFileUploadMaxMb: "Web SQLファイルの最大アップロードサイズ",
|
||||
webSqlFileUploadMaxMbDescription: "Webサーバーがストリーミング実行に受け付ける最大のSQLファイルサイズ({min}~{max} MB)。",
|
||||
sqlFileUploadMaxMbSaved: "Web SQLファイルのアップロードサイズを保存しました。",
|
||||
showColumnCommentsInHeader: "列名の下にコメントを表示",
|
||||
showColumnCommentsInHeaderDescription: "グリッド列名の直下にテーブル列コメントを表示します。",
|
||||
showColumnTypesInHeader: "列名の下にデータ型を表示",
|
||||
|
||||
@@ -5866,6 +5866,7 @@ export default withEnglishFallback({
|
||||
appearanceTab: "모양",
|
||||
navigationTab: "탐색",
|
||||
dataTab: "데이터",
|
||||
sqlFileSizeTab: "SQL 파일 크기",
|
||||
tunnelsTab: "터널",
|
||||
tunnelsDescription: "재사용 가능한 SSH/프록시/HTTP 터널 설정입니다. 한 번 구성한 후 연결의 터널 탭에서 프로필을 선택하세요. 여기서의 편집은 이 프로필을 사용하는 모든 연결에 적용됩니다.",
|
||||
tunnelsEmpty: "아직 터널 프로필이 없습니다. 여기서 만든 후 연결의 터널 설정에서 선택하세요.",
|
||||
@@ -6056,6 +6057,9 @@ export default withEnglishFallback({
|
||||
queryResultMaxRowsTooSmall: "쿼리 페이지 크기({pageSize}) 이상이어야 합니다.",
|
||||
externalSqlEditorMaxMb: "SQL 편집기 최대 파일 크기",
|
||||
externalSqlEditorMaxMbDescription: "이 크기를 초과하는 SQL 파일은 편집기 대신 스트리밍 실행 대화상자로 전달됩니다({min}–{max} MB).",
|
||||
webSqlFileUploadMaxMb: "웹 SQL 파일 최대 업로드 크기",
|
||||
webSqlFileUploadMaxMbDescription: "웹 서버가 스트리밍 실행에 허용하는 가장 큰 SQL 파일({min}–{max} MB).",
|
||||
sqlFileUploadMaxMbSaved: "웹 SQL 파일 업로드 크기가 저장되었습니다.",
|
||||
showColumnCommentsInHeader: "이름 아래에 컬럼 주석 표시",
|
||||
showColumnCommentsInHeaderDescription: "그리드 컬럼 이름 바로 아래에 테이블 컬럼 주석을 표시합니다.",
|
||||
showColumnTypesInHeader: "이름 아래에 컬럼 타입 표시",
|
||||
|
||||
@@ -6133,6 +6133,7 @@ export default withEnglishFallback({
|
||||
appearanceTab: "Aparência",
|
||||
navigationTab: "Navegação",
|
||||
dataTab: "Dados",
|
||||
sqlFileSizeTab: "Tamanho de arquivo SQL",
|
||||
tunnelsTab: "Túneis",
|
||||
tunnelsDescription: "Configurações reutilizáveis de túneis SSH / proxy / HTTP. Configure uma vez e selecione o perfil na aba de túnel de uma conexão; as edições aqui se aplicam a todas as conexões que usam o perfil.",
|
||||
tunnelsEmpty: "Ainda não há perfis de túnel. Crie um aqui e selecione-o nas configurações de túnel de uma conexão.",
|
||||
@@ -6325,6 +6326,9 @@ export default withEnglishFallback({
|
||||
queryResultMaxRowsTooSmall: "Deve ser pelo menos o tamanho da página da consulta ({pageSize}).",
|
||||
externalSqlEditorMaxMb: "Tamanho máximo de arquivo do editor SQL",
|
||||
externalSqlEditorMaxMbDescription: "Arquivos SQL maiores são enviados para o diálogo de execução em streaming em vez do editor ({min}–{max} MB).",
|
||||
webSqlFileUploadMaxMb: "Tamanho máximo de upload de arquivo SQL web",
|
||||
webSqlFileUploadMaxMbDescription: "Maior arquivo SQL que o servidor web aceita para execução em streaming ({min}–{max} MB).",
|
||||
sqlFileUploadMaxMbSaved: "Tamanho de upload de arquivo SQL web salvo.",
|
||||
showColumnCommentsInHeader: "Mostrar comentários de coluna sob os nomes",
|
||||
showColumnCommentsInHeaderDescription: "Exibir comentários de colunas da tabela diretamente abaixo dos nomes das colunas da grade.",
|
||||
showColumnTypesInHeader: "Mostrar tipos de coluna sob os nomes",
|
||||
|
||||
@@ -6426,6 +6426,7 @@ export default withEnglishFallback({
|
||||
appearanceTab: "Görünüm",
|
||||
navigationTab: "Gezinme",
|
||||
dataTab: "Veri",
|
||||
sqlFileSizeTab: "SQL Dosya Boyutu",
|
||||
tunnelsTab: "Tüneller",
|
||||
tunnelsDescription: "Yeniden kullanılabilir SSH / proxy / HTTP tünel yapılandırmaları. Bir kez yapılandırın, sonra bağlantının tünel sekmesinden profili seçin; buradaki değişiklikler profili kullanan her bağlantıda geçerli olur.",
|
||||
tunnelsEmpty: "Henüz tünel profili yok. Burada bir tane oluşturun, ardından bir bağlantının tünel ayarlarından seçin.",
|
||||
@@ -6627,6 +6628,9 @@ export default withEnglishFallback({
|
||||
queryResultMaxRowsTooSmall: "En az sorgu sayfa boyutu ({pageSize}) kadar olmalıdır.",
|
||||
externalSqlEditorMaxMb: "SQL düzenleyici maksimum dosya boyutu",
|
||||
externalSqlEditorMaxMbDescription: "Bu boyuttan büyük SQL dosyaları düzenleyici yerine akış yürütme iletişim kutusuna yönlendirilir ({min}–{max} MB).",
|
||||
webSqlFileUploadMaxMb: "Web SQL dosyası maksimum yükleme boyutu",
|
||||
webSqlFileUploadMaxMbDescription: "Web sunucusunun akış yürütme için kabul ettiği en büyük SQL dosyası ({min}–{max} MB).",
|
||||
sqlFileUploadMaxMbSaved: "Web SQL dosyası yükleme boyutu kaydedildi.",
|
||||
showColumnCommentsInHeader: "Sütun açıklamalarını adların altında göster",
|
||||
showColumnCommentsInHeaderDescription: "Tablo sütun açıklamalarını doğrudan ızgara sütun adlarının altında göster.",
|
||||
showColumnTypesInHeader: "Sütun türlerini adların altında göster",
|
||||
|
||||
@@ -6523,6 +6523,7 @@ export default withEnglishFallback({
|
||||
appearanceTab: "外观",
|
||||
navigationTab: "导航",
|
||||
dataTab: "数据",
|
||||
sqlFileSizeTab: "SQL 文件大小",
|
||||
tunnelsTab: "隧道维护",
|
||||
tunnelsDescription: "可复用的 SSH / 代理 / HTTP 隧道配置。一次配置,之后在连接的隧道页签中选择档案即可;此处的修改会对所有使用该档案的连接生效。",
|
||||
tunnelsEmpty: "还没有隧道档案。先在这里创建,然后在连接的隧道设置中选择使用。",
|
||||
@@ -6726,6 +6727,9 @@ export default withEnglishFallback({
|
||||
queryResultMaxRowsTooSmall: "不能小于查询结果默认每页行数({pageSize})。",
|
||||
externalSqlEditorMaxMb: "SQL 编辑器最大文件大小",
|
||||
externalSqlEditorMaxMbDescription: "超过此大小的 SQL 文件将转交流式执行 SQL 文件对话框而不是编辑器({min}–{max} MB)。",
|
||||
webSqlFileUploadMaxMb: "Web SQL 文件最大上传大小",
|
||||
webSqlFileUploadMaxMbDescription: "Web 服务器接受用于流式执行的最大 SQL 文件({min}–{max} MB)。",
|
||||
sqlFileUploadMaxMbSaved: "Web SQL 文件上传大小已保存。",
|
||||
showColumnCommentsInHeader: "在字段名下方显示注释",
|
||||
showColumnCommentsInHeaderDescription: "把表字段注释直接显示在结果表头字段名下方。",
|
||||
showColumnTypesInHeader: "在字段名下方显示数据类型",
|
||||
|
||||
@@ -5459,6 +5459,7 @@ export default withEnglishFallback({
|
||||
securityTab: "安全",
|
||||
aboutTab: "關於我們",
|
||||
dataTab: "資料",
|
||||
sqlFileSizeTab: "SQL 檔案大小",
|
||||
tunnelsTab: "隧道維護",
|
||||
tunnelsDescription: "可重複使用的 SSH / 代理 / HTTP 隧道設定。一次設定,之後在連線的隧道頁籤中選擇設定檔即可;此處的變更會套用到所有使用該設定檔的連線。",
|
||||
tunnelsEmpty: "還沒有隧道設定檔。先在這裡建立,然後在連線的隧道設定中選擇使用。",
|
||||
@@ -5638,6 +5639,9 @@ export default withEnglishFallback({
|
||||
queryResultMaxRowsTooSmall: "不能小於查詢結果預設每頁列數({pageSize})。",
|
||||
externalSqlEditorMaxMb: "SQL 編輯器最大檔案大小",
|
||||
externalSqlEditorMaxMbDescription: "超過此大小的 SQL 檔案將轉交流式執行 SQL 檔案對話方塊而不是編輯器({min}–{max} MB)。",
|
||||
webSqlFileUploadMaxMb: "Web SQL 檔案最大上傳大小",
|
||||
webSqlFileUploadMaxMbDescription: "Web 伺服器接受用於串流執行之最大 SQL 檔案({min}–{max} MB)。",
|
||||
sqlFileUploadMaxMbSaved: "Web SQL 檔案上傳大小已儲存。",
|
||||
showColumnCommentsInHeader: "在欄位名稱下方顯示註解",
|
||||
showColumnCommentsInHeaderDescription: "直接在資料表格欄位名稱下方顯示資料表欄位註解。",
|
||||
showColumnTypesInHeader: "在欄位名稱下方顯示資料類型",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -9,6 +9,13 @@ use tokio_util::sync::CancellationToken;
|
||||
use crate::connection::{AppState, PoolKind};
|
||||
use crate::db;
|
||||
use crate::models::connection::DatabaseType;
|
||||
|
||||
pub const DEFAULT_SQL_FILE_UPLOAD_MAX_MB: u32 = 200;
|
||||
pub const MAX_SQL_FILE_UPLOAD_MAX_MB: u32 = 4096;
|
||||
|
||||
pub fn clamp_sql_file_upload_max_mb(value: u32) -> u32 {
|
||||
value.clamp(1, MAX_SQL_FILE_UPLOAD_MAX_MB)
|
||||
}
|
||||
use crate::query::{
|
||||
execute_sql_statement_with_options, pool_error_action, wait_for_query_opt, DbOperationBudget, PoolErrorAction,
|
||||
QueryExecutionOptions,
|
||||
|
||||
@@ -38,6 +38,7 @@ const APP_STATE_TRANSFER_TASK_LIBRARY_KEY: &str = "transfer_task_library";
|
||||
const MCP_GLOBAL_POLICY_KEY: &str = "mcp_global_policy";
|
||||
const MCP_HTTP_SERVER_SETTINGS_KEY: &str = "mcp_http_server_settings";
|
||||
const MAX_RETRIES_KEY: &str = "max_retries";
|
||||
const SQL_FILE_UPLOAD_MAX_MB_KEY: &str = "sql_file_upload_max_mb";
|
||||
const APP_STATE_AI_GLOBAL_INSTRUCTIONS_KEY: &str = "ai_global_custom_instructions";
|
||||
const APP_STATE_AI_CHAT_SELECTION_KEY: &str = "ai_chat_selection_v1";
|
||||
const SNIPPET_SYNC_IDS_KEY: &str = "snippet_sync_ids";
|
||||
@@ -2050,7 +2051,7 @@ impl Storage {
|
||||
.query_row("SELECT settings_json FROM app_settings WHERE id = 1", [], |row| row.get(0))
|
||||
.optional()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let dedicated_keys = [MCP_GLOBAL_POLICY_KEY, MAX_RETRIES_KEY];
|
||||
let dedicated_keys = [MCP_GLOBAL_POLICY_KEY, MAX_RETRIES_KEY, SQL_FILE_UPLOAD_MAX_MB_KEY];
|
||||
for key in dedicated_keys {
|
||||
settings.remove(key);
|
||||
}
|
||||
@@ -2663,6 +2664,39 @@ impl Storage {
|
||||
.map(|value| crate::ai::clamp_max_retries(value.min(u32::MAX as u64) as u32))
|
||||
.unwrap_or(crate::ai::DEFAULT_MAX_RETRIES))
|
||||
}
|
||||
|
||||
pub async fn save_sql_file_upload_max_mb(&self, max_mb: u32) -> Result<(), String> {
|
||||
let max_mb = crate::sql_file_import::clamp_sql_file_upload_max_mb(max_mb);
|
||||
self.with_conn(move |conn| {
|
||||
let current: Option<String> = conn
|
||||
.query_row("SELECT settings_json FROM app_settings WHERE id = 1", [], |row| row.get(0))
|
||||
.optional()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut settings = match current {
|
||||
Some(json) => serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(&json)
|
||||
.map_err(|e| format!("invalid app settings JSON: {e}"))?,
|
||||
None => serde_json::Map::new(),
|
||||
};
|
||||
settings.insert(
|
||||
SQL_FILE_UPLOAD_MAX_MB_KEY.to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(max_mb)),
|
||||
);
|
||||
let json = serde_json::Value::Object(settings).to_string();
|
||||
conn.execute("INSERT OR REPLACE INTO app_settings (id, settings_json) VALUES (1, ?1)", [json])
|
||||
.map(|_| ())
|
||||
.map_err(|e| e.to_string())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn load_sql_file_upload_max_mb(&self) -> Result<u32, String> {
|
||||
let settings = self.load_app_settings_json().await?;
|
||||
Ok(settings
|
||||
.get(SQL_FILE_UPLOAD_MAX_MB_KEY)
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.map(|value| crate::sql_file_import::clamp_sql_file_upload_max_mb(value.min(u32::MAX as u64) as u32))
|
||||
.unwrap_or(crate::sql_file_import::DEFAULT_SQL_FILE_UPLOAD_MAX_MB))
|
||||
}
|
||||
}
|
||||
|
||||
// AI Conversations
|
||||
@@ -7197,6 +7231,39 @@ mod tests {
|
||||
assert_eq!(storage.load_max_retries().await.unwrap(), 7);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sql_file_upload_max_mb_defaults_and_persists_clamped() {
|
||||
let path = temp_db_path("sql-file-upload-max-mb");
|
||||
let storage = Storage::open(&path).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
storage.load_sql_file_upload_max_mb().await.unwrap(),
|
||||
crate::sql_file_import::DEFAULT_SQL_FILE_UPLOAD_MAX_MB
|
||||
);
|
||||
|
||||
storage.save_sql_file_upload_max_mb(512).await.unwrap();
|
||||
assert_eq!(storage.load_sql_file_upload_max_mb().await.unwrap(), 512);
|
||||
|
||||
// Values above the cap are clamped so raw DB edits cannot bypass the limit.
|
||||
storage.save_sql_file_upload_max_mb(u32::MAX).await.unwrap();
|
||||
assert_eq!(
|
||||
storage.load_sql_file_upload_max_mb().await.unwrap(),
|
||||
crate::sql_file_import::MAX_SQL_FILE_UPLOAD_MAX_MB
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sql_file_upload_max_mb_survives_stale_app_settings_save() {
|
||||
let path = temp_db_path("sql-file-upload-max-mb-stale-save");
|
||||
let storage = Storage::open(&path).await.unwrap();
|
||||
let stale_settings = storage.load_app_settings_json().await.unwrap();
|
||||
|
||||
storage.save_sql_file_upload_max_mb(256).await.unwrap();
|
||||
storage.save_app_settings_json(&stale_settings).await.unwrap();
|
||||
|
||||
assert_eq!(storage.load_sql_file_upload_max_mb().await.unwrap(), 256);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn password_hash_preserves_existing_desktop_settings() {
|
||||
let path = temp_db_path("password-preserve-desktop-settings");
|
||||
|
||||
@@ -1016,7 +1016,9 @@ async fn main() {
|
||||
.route(
|
||||
"/sql-file/preview",
|
||||
post(routes::sql_file::preview_sql_file)
|
||||
.layer(DefaultBodyLimit::max(routes::sql_file::SQL_FILE_UPLOAD_MAX_BYTES.saturating_add(1024 * 1024))),
|
||||
// Upper bound only; the effective (possibly lower) limit configured via
|
||||
// Settings > SQL File Size is enforced inside the handler at request time.
|
||||
.layer(DefaultBodyLimit::max(routes::sql_file::sql_file_upload_hard_cap_bytes())),
|
||||
)
|
||||
.route("/sql-file/execute", post(routes::sql_file::execute_sql_file))
|
||||
.route("/sql-file/progress/{executionId}", get(routes::sql_file::sql_file_progress))
|
||||
@@ -1057,6 +1059,11 @@ async fn main() {
|
||||
"/app-settings/max-retries",
|
||||
get(routes::app_settings::load_max_retries).put(routes::app_settings::save_max_retries),
|
||||
)
|
||||
.route(
|
||||
"/app-settings/sql-file-upload-max-bytes",
|
||||
get(routes::app_settings::load_sql_file_upload_max_bytes)
|
||||
.put(routes::app_settings::save_sql_file_upload_max_mb),
|
||||
)
|
||||
.route("/app-settings/config/decrypt", post(routes::app_settings::decrypt_config))
|
||||
// Cloud sync
|
||||
.route("/cloud-sync/webdav/test", post(routes::cloud_sync::webdav_sync_test))
|
||||
|
||||
@@ -130,6 +130,25 @@ pub async fn load_max_retries(State(state): State<Arc<WebState>>) -> Result<Json
|
||||
state.app.storage.load_max_retries().await.map(Json).map_err(AppError::from)
|
||||
}
|
||||
|
||||
pub async fn load_sql_file_upload_max_bytes(State(state): State<Arc<WebState>>) -> Result<Json<u32>, AppError> {
|
||||
let max_mb = state.app.storage.load_sql_file_upload_max_mb().await.map_err(AppError::from)?;
|
||||
Ok(Json(max_mb.saturating_mul(1024 * 1024)))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SaveSqlFileUploadMaxMbRequest {
|
||||
pub sql_file_upload_max_mb: u32,
|
||||
}
|
||||
|
||||
pub async fn save_sql_file_upload_max_mb(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(body): Json<SaveSqlFileUploadMaxMbRequest>,
|
||||
) -> Result<Json<()>, AppError> {
|
||||
state.app.storage.save_sql_file_upload_max_mb(body.sql_file_upload_max_mb).await.map_err(AppError::from)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
pub async fn save_max_retries(
|
||||
State(state): State<Arc<WebState>>,
|
||||
Json(body): Json<SaveMaxRetriesRequest>,
|
||||
|
||||
@@ -23,6 +23,31 @@ use crate::state::WebState;
|
||||
const PENDING_SQL_FILE_PROGRESS_CHANNEL_TTL: Duration = Duration::from_secs(30);
|
||||
const SQL_FILE_UPLOAD_MAX_AGE: Duration = Duration::from_secs(24 * 60 * 60);
|
||||
pub const SQL_FILE_UPLOAD_MAX_BYTES: usize = 200 * 1024 * 1024;
|
||||
const SQL_FILE_UPLOAD_MIN_BYTES: usize = 1024 * 1024;
|
||||
const SQL_FILE_UPLOAD_MAX_BYTES_LIMIT: usize = 4096 * 1024 * 1024;
|
||||
|
||||
pub fn sql_file_upload_max_bytes_from_value(value: Option<&str>) -> usize {
|
||||
let mb = value
|
||||
.and_then(|value| value.trim().parse::<usize>().ok())
|
||||
.filter(|mb| *mb > 0)
|
||||
.map(|mb| mb.saturating_mul(1024 * 1024))
|
||||
.unwrap_or(SQL_FILE_UPLOAD_MAX_BYTES);
|
||||
mb.clamp(SQL_FILE_UPLOAD_MIN_BYTES, SQL_FILE_UPLOAD_MAX_BYTES_LIMIT)
|
||||
}
|
||||
|
||||
pub fn sql_file_upload_hard_cap_bytes() -> usize {
|
||||
SQL_FILE_UPLOAD_MAX_BYTES_LIMIT.saturating_add(1024 * 1024)
|
||||
}
|
||||
|
||||
async fn sql_file_upload_limit(state: &WebState) -> usize {
|
||||
let max_mb = state
|
||||
.app
|
||||
.storage
|
||||
.load_sql_file_upload_max_mb()
|
||||
.await
|
||||
.unwrap_or(dbx_core::sql_file_import::DEFAULT_SQL_FILE_UPLOAD_MAX_MB);
|
||||
sql_file_upload_max_bytes_from_value(Some(&max_mb.to_string()))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -49,12 +74,9 @@ pub async fn preview_sql_file(
|
||||
if let Some(field) = multipart.next_field().await.map_err(|e| AppError::from(e.to_string()))? {
|
||||
let file_name = field.file_name().unwrap_or("upload.sql").to_string();
|
||||
let data = field.bytes().await.map_err(|e| AppError::from(e.to_string()))?;
|
||||
if data.len() > SQL_FILE_UPLOAD_MAX_BYTES {
|
||||
return Err(AppError::from(format!(
|
||||
"File too large: {} bytes (max {} bytes)",
|
||||
data.len(),
|
||||
SQL_FILE_UPLOAD_MAX_BYTES
|
||||
)));
|
||||
let upload_limit = sql_file_upload_limit(&state).await;
|
||||
if data.len() > upload_limit {
|
||||
return Err(AppError::from(format!("File too large: {} bytes (max {} bytes)", data.len(), upload_limit)));
|
||||
}
|
||||
|
||||
let file_path = safe_uploaded_sql_path(&tmp_dir, &file_name)?;
|
||||
@@ -135,12 +157,13 @@ pub async fn execute_sql_file(
|
||||
None,
|
||||
));
|
||||
for file_path in &file_paths {
|
||||
let upload_limit = sql_file_upload_limit(&state_clone).await;
|
||||
match std::fs::metadata(file_path) {
|
||||
Ok(meta) if meta.len() > SQL_FILE_UPLOAD_MAX_BYTES as u64 => {
|
||||
Ok(meta) if meta.len() > upload_limit as u64 => {
|
||||
progress_emitter.emit(sql_file_error_progress(
|
||||
&req.execution_id,
|
||||
started_at,
|
||||
format!("File too large: {} bytes (max {} bytes)", meta.len(), SQL_FILE_UPLOAD_MAX_BYTES),
|
||||
format!("File too large: {} bytes (max {} bytes)", meta.len(), upload_limit),
|
||||
));
|
||||
cleanup_sql_file_execution(&state_clone, &req.execution_id).await;
|
||||
return;
|
||||
@@ -278,7 +301,20 @@ pub async fn cancel_sql_file(
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use super::{cleanup_sql_file_uploads_older_than, safe_uploaded_sql_path, validated_uploaded_sql_path};
|
||||
use super::{
|
||||
cleanup_sql_file_uploads_older_than, safe_uploaded_sql_path, sql_file_upload_max_bytes_from_value,
|
||||
validated_uploaded_sql_path, SQL_FILE_UPLOAD_MAX_BYTES,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn sql_file_upload_limit_defaults_and_clamps() {
|
||||
assert_eq!(sql_file_upload_max_bytes_from_value(None), SQL_FILE_UPLOAD_MAX_BYTES);
|
||||
assert_eq!(sql_file_upload_max_bytes_from_value(Some("")), SQL_FILE_UPLOAD_MAX_BYTES);
|
||||
assert_eq!(sql_file_upload_max_bytes_from_value(Some("0")), SQL_FILE_UPLOAD_MAX_BYTES);
|
||||
assert_eq!(sql_file_upload_max_bytes_from_value(Some("512")), 512 * 1024 * 1024);
|
||||
assert_eq!(sql_file_upload_max_bytes_from_value(Some("1")), 1024 * 1024);
|
||||
assert_eq!(sql_file_upload_max_bytes_from_value(Some("99999")), 4096 * 1024 * 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uploaded_sql_paths_are_unique_and_keep_the_extension() {
|
||||
|
||||
Reference in New Issue
Block a user