From 2c463144251a91b8e49b2652384a20f82f7323f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 24 Jul 2026 13:39:40 +0200 Subject: [PATCH] reverse proxy info --- Cargo.toml | 2 +- src/app.rs | 9 ++++++++- static/js/api.js | 23 +++++++++++++++++++++++ static/js/editor-format.js | 6 +++++- 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3312bc9..6b51a38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustpad" -version = "0.0.15" +version = "0.0.16" edition = "2024" rust-version = "1.94" description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL" diff --git a/src/app.rs b/src/app.rs index dbd4d32..9b0bc60 100644 --- a/src/app.rs +++ b/src/app.rs @@ -177,6 +177,7 @@ async fn home(State(state): State) -> Response { &state.asset_version, state.registration_enabled, &state.frontend_log_level, + state.upload_max_size_bytes, ) } @@ -190,6 +191,7 @@ async fn pad(State(state): State, Path(slug): Path) -> Resp &state.asset_version, state.registration_enabled, &state.frontend_log_level, + state.upload_max_size_bytes, ) } Ok(None) => error_response( @@ -215,6 +217,7 @@ async fn public_page(State(state): State, Path(token): Path &state.asset_version, state.registration_enabled, &state.frontend_log_level, + state.upload_max_size_bytes, ), Ok(None) => error_response( StatusCode::NOT_FOUND, @@ -245,6 +248,7 @@ async fn workspace( &state.asset_version, state.registration_enabled, &state.frontend_log_level, + state.upload_max_size_bytes, ) } Ok(None) => error_response( @@ -297,6 +301,7 @@ async fn note( &state.asset_version, state.registration_enabled, &state.frontend_log_level, + state.upload_max_size_bytes, ) } Ok(None) => error_response( @@ -390,10 +395,12 @@ fn versioned_html( asset_version: &str, registration_enabled: bool, frontend_log_level: &str, + upload_max_size_bytes: usize, ) -> Response { let frontend_config = format!( - r#""#, + r#""#, escape_js_string(frontend_log_level), + upload_max_size_bytes, ); let html = template .replace("__ASSET_VERSION__", asset_version) diff --git a/static/js/api.js b/static/js/api.js index 0980ea1..3fb66dd 100644 --- a/static/js/api.js +++ b/static/js/api.js @@ -1,6 +1,26 @@ import { logDebug, logError, logWarn } from "./logger.js"; +function formatBytes(bytes) { + if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(bytes % (1024 * 1024) ? 1 : 0)} MB`; + if (bytes >= 1024) return `${Math.ceil(bytes / 1024)} KB`; + return `${bytes} B`; +} + +function validateUploadSize(body) { + if (!(body instanceof FormData)) return; + const maxBytes = Number(window.__RUSTPAD_CONFIG__?.uploadMaxSizeBytes || 0); + if (!Number.isFinite(maxBytes) || maxBytes <= 0) return; + for (const value of body.values()) { + if (value instanceof File && value.size > maxBytes) { + const error = new Error(`The selected file is ${formatBytes(value.size)}. The upload limit is ${formatBytes(maxBytes)}.`); + error.status = 413; + throw error; + } + } +} + export async function api(path, options = {}) { + validateUploadSize(options.body); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 12000); try { @@ -24,6 +44,9 @@ export async function api(path, options = {}) { } catch (error) { if (error.name === "AbortError") { logWarn("api.timeout", { method: options.method || "GET", path }); throw new Error("Timed out"); } logError("api.network_error", error, { method: options.method || "GET", path }); + if (options.body instanceof FormData && error instanceof TypeError) { + throw new Error("Upload failed before the server returned a response. The file may exceed the server or proxy upload limit."); + } throw error; } finally { clearTimeout(timeout); } } diff --git a/static/js/editor-format.js b/static/js/editor-format.js index daa9ef4..63a557f 100644 --- a/static/js/editor-format.js +++ b/static/js/editor-format.js @@ -53,7 +53,11 @@ export function applyFormat(editor, format) { if (format === "codeblock-lines") toggleWrap(editor, "```text=\n", "\n```", "code"); if (format === "mermaid") toggleWrap(editor, "```mermaid\n", "\n```", "graph TD\n A[Start] --> B[End]"); if (format === "details") toggleWrap(editor, "
\nClick me\n\n", "\n
", "Content"); - if (format === "toc") toggleWrap(editor, "", "", "[TOC]"); + if (format === "toc") { + const { start, end } = selection(editor); + const selected = editor.value.slice(start, end); + editor.setRangeText(selected || "[TOC]", start, end, selected ? "select" : "end"); + } if (format.startsWith("alert-")) { const type = format.slice("alert-".length); toggleWrap(editor, `:::${type}\n`, "\n:::", "Alert content");