fix: Fix the error when uploading files with Unicode characters in filenames

This commit is contained in:
Felix
2026-03-24 19:33:12 +08:00
parent db86ff4ce3
commit da12f47369
4 changed files with 91 additions and 26 deletions
Generated
+24
View File
@@ -404,6 +404,7 @@ dependencies = [
"matchit",
"memchr",
"mime",
"multer",
"percent-encoding",
"pin-project-lite",
"serde_core",
@@ -3481,6 +3482,23 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "multer"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
dependencies = [
"bytes",
"encoding_rs",
"futures-util",
"http",
"httparse",
"memchr",
"mime",
"spin",
"version_check",
]
[[package]]
name = "native-tls"
version = "0.2.18"
@@ -6038,6 +6056,12 @@ dependencies = [
"system-deps",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "spinning_top"
version = "0.3.0"
+1 -1
View File
@@ -84,7 +84,7 @@ url = "2"
wasmtime = "41"
# HTTP server (for API daemon)
axum = { version = "0.8", features = ["ws"] }
axum = { version = "0.8", features = ["ws", "multipart"] }
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip", "compression-br"] }
+61 -20
View File
@@ -1,7 +1,7 @@
//! Route handlers for the OpenFang API.
use crate::types::*;
use axum::extract::{Path, Query, State};
use axum::extract::{Multipart, Path, Query, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Json;
@@ -9319,16 +9319,14 @@ fn is_allowed_content_type(ct: &str) -> bool {
/// POST /api/agents/{id}/upload — Upload a file attachment.
///
/// Accepts raw body bytes. The client must set:
/// - `Content-Type` header (e.g., `image/png`, `text/plain`, `application/pdf`)
/// - `X-Filename` header (original filename)
/// Accepts multipart/form-data. The client must include a file field with:
/// - `Content-Type` field (e.g., `image/png`, `text/plain`, `application/pdf`)
/// - `filename` attribute (original filename)
pub async fn upload_file(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
headers: axum::http::HeaderMap,
body: axum::body::Bytes,
mut multipart: Multipart,
) -> impl IntoResponse {
// Validate agent ID format
let _agent_id: AgentId = match id.parse() {
Ok(id) => id,
Err(_) => {
@@ -9339,12 +9337,62 @@ pub async fn upload_file(
}
};
// Extract content type
let content_type = headers
.get(axum::http::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("application/octet-stream")
.to_string();
let mut file_data: Option<(Option<String>, String, axum::body::Bytes)> = None;
let mut filename_from_field: Option<String> = None;
while let Some(field) = multipart.next_field().await.transpose() {
let field = match field {
Ok(f) => f,
Err(e) => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": format!("Failed to read field: {}", e)})),
);
}
};
let field_name = field.name().unwrap_or("").to_string();
match field_name.as_str() {
"file" => {
let filename_attr = field.file_name().map(|s| s.to_string());
let content_type = field
.content_type()
.unwrap_or("application/octet-stream")
.to_string();
let bytes = match field.bytes().await {
Ok(b) => b,
Err(e) => {
return (
StatusCode::BAD_REQUEST,
Json(
serde_json::json!({"error": format!("Failed to read file: {}", e)}),
),
);
}
};
file_data = Some((filename_attr, content_type, bytes));
}
"filename" => {
filename_from_field = field.text().await.ok();
}
_ => {}
}
}
let (filename_attr, content_type, body) = match file_data {
Some(data) => data,
None => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "No file provided"})),
);
}
};
let filename = filename_from_field
.or(filename_attr)
.unwrap_or_else(|| "upload".to_string());
if !is_allowed_content_type(&content_type) {
return (
@@ -9355,13 +9403,6 @@ pub async fn upload_file(
);
}
// Extract filename from header
let filename = headers
.get("X-Filename")
.and_then(|v| v.to_str().ok())
.unwrap_or("upload")
.to_string();
// Validate size
if body.len() > MAX_UPLOAD_SIZE {
return (
+5 -5
View File
@@ -297,15 +297,15 @@ var OpenFangAPI = (function() {
function getToken() { return _authToken; }
function upload(agentId, file) {
var hdrs = {
'Content-Type': file.type || 'application/octet-stream',
'X-Filename': file.name
};
var hdrs = {};
if (_authToken) hdrs['Authorization'] = 'Bearer ' + _authToken;
var form = new FormData();
form.append('file', file);
form.append('filename', file.name);
return fetch(BASE + '/api/agents/' + agentId + '/upload', {
method: 'POST',
headers: hdrs,
body: file
body: form
}).then(function(r) {
if (!r.ok) throw new Error('Upload failed');
return r.json();