From 66154e074f5f6d5709ee13b8cc18740e1c73f6b9 Mon Sep 17 00:00:00 2001 From: Mikhail Mitrofanov Date: Tue, 11 Aug 2026 13:50:08 +0300 Subject: [PATCH] New upload api --- Cargo.lock | 82 ++++++++++++++++++++++++------------------------- Cargo.toml | 10 ++++-- src/main.rs | 88 +++++++++++++++++++++++++++++------------------------ 3 files changed, 95 insertions(+), 85 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 51ef20b..5eb3a54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,7 +69,6 @@ dependencies = [ "matchit", "memchr", "mime", - "multer", "percent-encoding", "pin-project-lite", "serde_core", @@ -287,15 +286,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -348,6 +338,23 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + [[package]] name = "futures-task" version = "0.3.32" @@ -361,6 +368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", + "futures-macro", "futures-task", "pin-project-lite", "slab", @@ -542,8 +550,12 @@ version = "0.1.0" dependencies = [ "axum", "criterion", + "futures-util", + "http-body-util", + "parking_lot", "rand", "tokio", + "tokio-util", "tower-http", ] @@ -603,23 +615,6 @@ dependencies = [ "windows-sys", ] -[[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 = "num-traits" version = "0.2.19" @@ -750,9 +745,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ "chacha20", "getrandom", @@ -960,12 +955,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "syn" version = "2.0.117" @@ -995,9 +984,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -1021,6 +1010,19 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower" version = "0.5.3" @@ -1098,12 +1100,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 2379daa..967b578 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,10 +4,14 @@ version = "0.1.0" edition = "2024" [dependencies] -axum = { version = "0.8.9", features = ["multipart", ] } -tokio = { version = "1.52.3", features = ["full"] } -rand = "0.10.1" +axum = { version = "0.8.9" } +tokio = { version = "1.53.1", features = ["full"] } +tokio-util = { version = "0.7.19", features = ["io"] } +rand = "0.10.2" tower-http = {version = "0.7.0", features = ["cors", "trace"]} +parking_lot = "0.12.5" +http-body-util = "0.1.3" +futures-util = "0.3.32" [dev-dependencies] criterion = { version = "0.8.2", features = ["html_reports"] } diff --git a/src/main.rs b/src/main.rs index 84b224c..c01de9a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,19 @@ -use axum::extract::{DefaultBodyLimit, Path}; -use axum::http::StatusCode; +use axum::body::Body; +use axum::extract::{DefaultBodyLimit, Path, State}; use axum::http::Method; - +use axum::http::StatusCode; use axum::response::IntoResponse; use axum::routing::post; -use axum::{extract::Multipart, routing::get, Router}; +use axum::{routing::get, Router}; +use futures_util::TryStreamExt; +use http_body_util::BodyExt; +use parking_lot::RwLock; use rand::distr::{Alphanumeric, SampleString}; use rand::rng; use std::os::unix::prelude::MetadataExt; use std::path::PathBuf; -use tokio::fs::File; +use std::sync::Arc; +use tokio_util::io::StreamReader; use tower_http::cors::{Any, CorsLayer}; #[tokio::main] @@ -17,58 +21,66 @@ async fn main() { ensure_work_dir(); let cors = CorsLayer::new() .allow_origin(Any) - .allow_methods(vec![Method::GET, Method::POST]).allow_headers(Any); + .allow_methods(vec![Method::GET, Method::POST]) + .allow_headers(Any); let app = Router::new() .route("/generate", get(generate_id)) .route("/upload/{id}", post(upload_file)) .route("/download/{id}", get(download_file)) .route("/{id}/version", get(version)) - .layer(DefaultBodyLimit::max(50 * 1024 * 1024)) - .layer(cors); - + .layer(DefaultBodyLimit::max(5 * 1024 * 1024)) + .layer(cors) + .with_state(Arc::new(RwLock::new(ApiState { + work_directory: get_dir(), + temp_directory: PathBuf::from("/tmp"), + }))); let listener = tokio::net::TcpListener::bind("0.0.0.0:8089").await.unwrap(); axum::serve(listener, app).await.unwrap(); } +struct ApiState { + work_directory: PathBuf, + temp_directory: PathBuf, +} + async fn generate_id() -> String { Alphanumeric.sample_string(&mut rng(), 24).to_uppercase() } async fn upload_file( + State(state): State>>, Path(id): Path, - mut multipart: Multipart, + body: Body, ) -> Result { if !validate_id(id.as_str()) { return Err(StatusCode::FORBIDDEN); } - let file = multipart.next_field().await; - - if let Ok(Some(file)) = file { - let data = file - .bytes() - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - let mut path = get_dir(); - path.push(id.clone()); - if !path.exists() { - File::create(path.clone()) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - } - - tokio::fs::write(path.clone(), data) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - - let file = File::open(path).await.unwrap(); - Ok(file.metadata().await.unwrap().mtime().to_string()) - } else { - Err(StatusCode::BAD_REQUEST) + let temp_path; + let store_path; + { + let state = state.read(); + temp_path = state.temp_directory.join(id.as_str()); + store_path = state.work_directory.join(id.as_str()); } + + let mut temp_file = tokio::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&temp_path) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let stream = TryStreamExt::map_err(body.into_data_stream(), std::io::Error::other); + let mut reader = StreamReader::new(stream); + tokio::io::copy(&mut reader, &mut temp_file).await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + tokio::fs::rename(temp_path, store_path).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(()) } async fn download_file(Path(id): Path) -> Result, StatusCode> { @@ -91,7 +103,8 @@ async fn version(Path(id): Path) -> Result PathBuf { #[inline] fn validate_id(id: &str) -> bool { - id.len() == 24 - && id - .bytes() - .all(|b| matches!(b, b'A'..=b'Z' | b'0'..=b'9')) + id.len() == 24 && id.bytes().all(|b| matches!(b, b'A'..=b'Z' | b'0'..=b'9')) }