From b7c36074b95bbdb0865e667fa6220a8825c6138a Mon Sep 17 00:00:00 2001 From: kdletters Date: Sun, 19 Jul 2026 17:54:09 +0800 Subject: [PATCH] =?UTF-8?q?=E7=83=AD=E4=BF=AE=E5=9B=BE=E7=89=87=E7=94=BB?= =?UTF-8?q?=E5=B8=83=E5=B8=83=E5=B1=80=E4=B8=8A=E9=99=90=E8=87=B32MiB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 API 与 SpacetimeDB 的画布布局校验上限统一提高到 2 MiB 为站内与外部画布保存接口增加 4 MiB 请求体上限 补充 2 MiB 边界通过和超限拒绝测试 --- .../crates/api-server/src/editor_project.rs | 21 ++++++++++++++- .../api-server/src/modules/editor_project.rs | 18 ++++++++----- .../api-server/src/modules/external_api.rs | 15 +++++++---- .../src/editor_project_storage.rs | 26 ++++++++++++++++++- 4 files changed, 66 insertions(+), 14 deletions(-) diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index a87179325..d3f52d6ea 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -96,7 +96,8 @@ pub(crate) const EDITOR_PROJECT_ID_PREFIX: &str = "proj-"; pub(crate) const EDITOR_RESOURCE_ID_PREFIX: &str = "editor-resource-"; pub(crate) const EDITOR_ASSET_FOLDER_ID_PREFIX: &str = "editor-asset-folder-"; pub(crate) const EDITOR_ASSET_ID_PREFIX: &str = "editor-asset-"; -const EDITOR_LAYOUT_MAX_BYTES: usize = 256 * 1024; +const EDITOR_LAYOUT_MAX_BYTES: usize = 2 * 1024 * 1024; +pub(crate) const EDITOR_LAYOUT_REQUEST_BODY_MAX_BYTES: usize = 4 * 1024 * 1024; pub(crate) const EDITOR_PROJECT_DEFAULT_TITLE: &str = "未命名画布"; const EDITOR_ASSET_METADATA_MAX_BYTES: usize = 64 * 1024; const EDITOR_ASSET_DEFAULT_FOLDER_ID: &str = "project"; @@ -8750,6 +8751,24 @@ mod tests { ); } + #[test] + fn editor_layout_serialization_accepts_payload_up_to_two_mib() { + let payload = json!("x".repeat(EDITOR_LAYOUT_MAX_BYTES - 2)); + + let serialized = serialize_editor_layers(payload).expect("two MiB layout should save"); + + assert_eq!(serialized.len(), EDITOR_LAYOUT_MAX_BYTES); + } + + #[test] + fn editor_layout_serialization_rejects_payload_over_two_mib() { + let payload = json!("x".repeat(EDITOR_LAYOUT_MAX_BYTES - 1)); + + let error = serialize_editor_layers(payload).expect_err("oversized layout should fail"); + + assert!(error.body_text().contains("图层布局过大")); + } + #[test] fn editor_asset_metadata_serialization_rejects_nested_inline_media() { let error = serialize_editor_asset_metadata(Some(json!({ diff --git a/server-rs/crates/api-server/src/modules/editor_project.rs b/server-rs/crates/api-server/src/modules/editor_project.rs index 547443534..90747e184 100644 --- a/server-rs/crates/api-server/src/modules/editor_project.rs +++ b/server-rs/crates/api-server/src/modules/editor_project.rs @@ -1,6 +1,7 @@ use axum::{ Router, extract::DefaultBodyLimit, + handler::Handler, middleware, routing::{get, patch, post}, }; @@ -14,12 +15,12 @@ use crate::{ get_editor_agent_conversation, list_editor_agent_conversations, }, editor_project::{ - create_editor_asset, create_editor_asset_folder, create_editor_project, - create_editor_project_resource, delete_editor_asset, delete_editor_asset_folder, - delete_editor_project, edit_editor_image, extract_editor_ui_design_assets, - generate_editor_icon_spritesheet, generate_editor_image, get_editor_asset_library, - get_editor_generation_pricing, get_editor_project, list_editor_projects, - list_public_editor_project_resources, load_recent_editor_project, + EDITOR_LAYOUT_REQUEST_BODY_MAX_BYTES, create_editor_asset, create_editor_asset_folder, + create_editor_project, create_editor_project_resource, delete_editor_asset, + delete_editor_asset_folder, delete_editor_project, edit_editor_image, + extract_editor_ui_design_assets, generate_editor_icon_spritesheet, generate_editor_image, + get_editor_asset_library, get_editor_generation_pricing, get_editor_project, + list_editor_projects, list_public_editor_project_resources, load_recent_editor_project, remove_editor_image_background, rename_editor_project, save_editor_project_layout, split_editor_icon_spritesheet, submit_editor_asset_showcase, toggle_editor_showcase_asset_like, update_editor_asset, update_editor_asset_folder, @@ -59,7 +60,10 @@ pub fn router(state: AppState) -> Router { .route( "/api/editor/projects/{project_id}", get(get_editor_project) - .patch(save_editor_project_layout) + .patch( + save_editor_project_layout + .layer(DefaultBodyLimit::max(EDITOR_LAYOUT_REQUEST_BODY_MAX_BYTES)), + ) .delete(delete_editor_project) .route_layer(middleware::from_fn_with_state( state.clone(), diff --git a/server-rs/crates/api-server/src/modules/external_api.rs b/server-rs/crates/api-server/src/modules/external_api.rs index 497ade677..a3341c2ee 100644 --- a/server-rs/crates/api-server/src/modules/external_api.rs +++ b/server-rs/crates/api-server/src/modules/external_api.rs @@ -1,9 +1,12 @@ use axum::{ - Router, middleware, + Router, + extract::DefaultBodyLimit, + middleware, routing::{get, patch, post}, }; use crate::{ + editor_project::EDITOR_LAYOUT_REQUEST_BODY_MAX_BYTES, external_api_auth::require_external_api_key, external_assets_api::{ confirm_external_asset_object, create_external_direct_upload_ticket, @@ -84,10 +87,12 @@ pub fn router(state: AppState) -> Router { ) .route( "/api/external/v1/editor/projects/{project_id}/canvas", - patch(save_external_editor_canvas).route_layer(middleware::from_fn_with_state( - state.clone(), - require_external_api_key, - )), + patch(save_external_editor_canvas) + .layer(DefaultBodyLimit::max(EDITOR_LAYOUT_REQUEST_BODY_MAX_BYTES)) + .route_layer(middleware::from_fn_with_state( + state.clone(), + require_external_api_key, + )), ) .route( "/api/external/v1/editor/projects/{project_id}/resources", diff --git a/server-rs/crates/spacetime-module/src/editor_project_storage.rs b/server-rs/crates/spacetime-module/src/editor_project_storage.rs index 19f80f00c..70d8f8d52 100644 --- a/server-rs/crates/spacetime-module/src/editor_project_storage.rs +++ b/server-rs/crates/spacetime-module/src/editor_project_storage.rs @@ -7,7 +7,7 @@ const EDITOR_ASSET_DEFAULT_FOLDER_ID: &str = "project"; const EDITOR_ASSET_DEFAULT_FOLDER_LABEL: &str = "项目素材"; const EDITOR_PROJECT_MAX_TITLE_CHARS: usize = 80; const EDITOR_ASSET_MAX_LABEL_CHARS: usize = 80; -const EDITOR_PROJECT_MAX_LAYOUT_JSON_BYTES: usize = 256 * 1024; +const EDITOR_PROJECT_MAX_LAYOUT_JSON_BYTES: usize = 2 * 1024 * 1024; const EDITOR_PROJECT_SOURCE_TYPES: [&str; 3] = ["uploaded", "generated", "mock_generated"]; const EDITOR_SHOWCASE_STATUS_PENDING: &str = "pending"; const EDITOR_SHOWCASE_STATUS_APPROVED: &str = "approved"; @@ -4997,6 +4997,30 @@ mod tests { use super::*; use serde_json::json; + #[test] + fn editor_layout_accepts_json_up_to_two_mib() { + let payload = format!( + "\"{}\"", + "x".repeat(EDITOR_PROJECT_MAX_LAYOUT_JSON_BYTES - 2) + ); + + let normalized = normalize_layout_json(payload).expect("two MiB layout should save"); + + assert_eq!(normalized.len(), EDITOR_PROJECT_MAX_LAYOUT_JSON_BYTES); + } + + #[test] + fn editor_layout_rejects_json_over_two_mib() { + let payload = format!( + "\"{}\"", + "x".repeat(EDITOR_PROJECT_MAX_LAYOUT_JSON_BYTES - 1) + ); + + let error = normalize_layout_json(payload).expect_err("oversized layout should fail"); + + assert_eq!(error, "图片画布图层布局过大"); + } + fn public_showcase_asset() -> EditorShowcaseAsset { let now = Timestamp::from_micros_since_unix_epoch(1_000_000); EditorShowcaseAsset {