From 5bb2a6c192b167991c2f05652f512bb38765d378 Mon Sep 17 00:00:00 2001 From: Pakin Date: Mon, 27 Apr 2026 15:47:11 +0700 Subject: [PATCH] change: change get from json cache file to multipart Signed-off-by: Pakin --- Cargo.lock | 51 ++++++++++++++++- Cargo.toml | 1 + src/app.rs | 159 +++++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 187 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a3f576..32470fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -247,9 +247,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "bytes", @@ -297,6 +297,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970" +dependencies = [ + "axum", + "axum-core", + "bytes", + "fastrand", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "multer", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-macros" version = "0.5.0" @@ -1667,6 +1690,23 @@ dependencies = [ "pxfm", ] +[[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 = "multimap" version = "0.10.1" @@ -2614,6 +2654,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2695,6 +2741,7 @@ version = "0.1.0" dependencies = [ "async-compression", "axum", + "axum-extra", "axum-macros", "bb8", "bb8-redis", diff --git a/Cargo.toml b/Cargo.toml index 9ec73b0..d15588c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" async-compression = { version = "0.4.39", features = ["tokio", "brotli"] } axum = "0.8.7" axum-macros = "0.5.0" +axum-extra = { version = "0.12.6", features = ["multipart"] } bb8 = "0.9.1" bb8-redis = "0.26.0" brotli = "8.0.2" diff --git a/src/app.rs b/src/app.rs index 1e85aa0..bfa0e2f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -8,6 +8,7 @@ use axum::{ response::{IntoResponse, Response}, routing::{get, post}, }; +use axum_extra::extract::multipart::Multipart; use axum_macros::debug_handler; use bb8::{Pool, PooledConnection}; use bb8_redis::RedisConnectionManager; @@ -378,37 +379,148 @@ struct Signature { #[debug_handler] async fn commit_handler( State(state): State, - // request body - Json(payload): Json, + // request body as multipart/form-data + mut payload: Multipart, ) -> impl IntoResponse { - let mut content = match fetch_content_from_redis(state.redis.clone(), &payload.patch_key).await - { - Ok(c) => c, - Err(e) => { + // Extract multipart fields + let mut path: Option = None; + let mut signature_username: Option = None; + let mut signature_email: Option = None; + let mut message: Option = None; + let mut file_bytes: Option> = None; + + // Process each field in the multipart payload + while let Ok(Some(field)) = payload.next_field().await { + let name = field.name().unwrap_or("").to_string(); + + match name.as_str() { + "path" => { + path = Some(match field.text().await { + Ok(t) => t, + Err(e) => { + return ( + axum::http::StatusCode::BAD_REQUEST, + Json(json!({"error": format!("Failed to read path field: {}", e)})), + ); + } + }); + } + "signature_username" => { + signature_username = Some(match field.text().await { + Ok(t) => t, + Err(e) => { + return ( + axum::http::StatusCode::BAD_REQUEST, + Json( + json!({"error": format!("Failed to read signature_username field: {}", e)}), + ), + ); + } + }); + } + "signature_email" => { + signature_email = Some(match field.text().await { + Ok(t) => t, + Err(e) => { + return ( + axum::http::StatusCode::BAD_REQUEST, + Json( + json!({"error": format!("Failed to read signature_email field: {}", e)}), + ), + ); + } + }); + } + "message" => { + message = Some(match field.text().await { + Ok(t) => t, + Err(e) => { + return ( + axum::http::StatusCode::BAD_REQUEST, + Json(json!({"error": format!("Failed to read message field: {}", e)})), + ); + } + }); + } + "file" => { + file_bytes = Some(match field.bytes().await { + Ok(b) => b.to_vec(), + Err(e) => { + return ( + axum::http::StatusCode::BAD_REQUEST, + Json(json!({"error": format!("Failed to read file field: {}", e)})), + ); + } + }); + } + _ => { + // Ignore unknown fields + } + } + } + + // Validate required fields + let path = match path { + Some(p) => p, + None => { return ( axum::http::StatusCode::BAD_REQUEST, - Json(json!({"error": e})), + Json(json!({"error": "Missing required field: path"})), ); } }; - let is_patch_file = content.starts_with("patch"); - // do apply patch first - if is_patch_file { - content = apply_patch_to_file(state.redis.clone(), &payload.path, &mut content).await; - } + let signature_username = match signature_username { + Some(su) => su, + None => { + return ( + axum::http::StatusCode::BAD_REQUEST, + Json(json!({"error": "Missing required field: signature_username"})), + ); + } + }; + let signature_email = match signature_email { + Some(se) => se, + None => { + return ( + axum::http::StatusCode::BAD_REQUEST, + Json(json!({"error": "Missing required field: signature_email"})), + ); + } + }; + + let file_bytes = match file_bytes { + Some(fb) => fb, + None => { + return ( + axum::http::StatusCode::BAD_REQUEST, + Json(json!({"error": "Missing required field: file"})), + ); + } + }; + + // Create signature + let signature = Signature { + username: signature_username, + email: signature_email, + }; + + // Get branch name from config + let branch = state + .clone() + .get_config("GIT_REPO_BRANCH_NAME") + .map(|x| x.to_string()) + .unwrap_or("master".to_string()); + + // Commit the file content directly from multipart upload let commit_oid = match commit_file_content( state.clone().repo, - &payload.path, - &content.as_bytes(), - payload.signature, - &payload.message.unwrap_or("update: from api".to_string()), - state - .clone() - .get_config("GIT_REPO_BRANCH_NAME") - .map(|x| x.to_string()) - .unwrap_or("master".to_string()), + &path, + &file_bytes, + signature, + &message.unwrap_or("update: from api".to_string()), + branch, ) .await { @@ -425,7 +537,10 @@ async fn commit_handler( let redis_pre_lock = state.redis.clone(); { if let Ok(mut rl) = redis_pre_lock.get().await { - let _ = rl.rpush(format!("{}.history", payload.path), payload.patch_key); + let _ = rl.rpush( + format!("{}.history", path), + format!("commit-{}", commit_oid), + ); } }