diff --git a/Cargo.lock b/Cargo.lock index 5eb3a54..cb175c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,6 +69,7 @@ dependencies = [ "matchit", "memchr", "mime", + "multer", "percent-encoding", "pin-project-lite", "serde_core", @@ -286,6 +287,15 @@ 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" @@ -551,7 +561,6 @@ dependencies = [ "axum", "criterion", "futures-util", - "http-body-util", "parking_lot", "rand", "tokio", @@ -615,6 +624,23 @@ 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" @@ -955,6 +981,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + [[package]] name = "syn" version = "2.0.117" @@ -1100,6 +1132,12 @@ 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 967b578..10bc7f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,13 +4,12 @@ version = "0.1.0" edition = "2024" [dependencies] -axum = { version = "0.8.9" } +axum = { version = "0.8.9", features = ["multipart"] } 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] diff --git a/src/main.rs b/src/main.rs index c01de9a..c7954c0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,20 @@ use axum::body::Body; -use axum::extract::{DefaultBodyLimit, Path, State}; +use axum::extract::{DefaultBodyLimit, Multipart, Path, State}; use axum::http::Method; use axum::http::StatusCode; -use axum::response::IntoResponse; +use axum::response::{IntoResponse, Response}; use axum::routing::post; 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 std::sync::Arc; -use tokio_util::io::StreamReader; +use axum::http::header::CONTENT_TYPE; +use tokio::fs::File; +use tokio_util::io::{ReaderStream, StreamReader}; use tower_http::cors::{Any, CorsLayer}; #[tokio::main] @@ -26,6 +27,7 @@ async fn main() { let app = Router::new() .route("/generate", get(generate_id)) + .route("/upload/stream/{id}", post(upload_file_stream)) .route("/upload/{id}", post(upload_file)) .route("/download/{id}", get(download_file)) .route("/{id}/version", get(version)) @@ -33,7 +35,6 @@ async fn main() { .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(); @@ -42,28 +43,27 @@ async fn main() { 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( +async fn upload_file_stream( State(state): State>>, Path(id): Path, body: Body, ) -> Result { if !validate_id(id.as_str()) { - return Err(StatusCode::FORBIDDEN); + return 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()); + temp_path = state.work_directory.join(format!("{id}.tmp")); } let mut temp_file = tokio::fs::OpenOptions::new() @@ -83,23 +83,62 @@ async fn upload_file( Ok(()) } -async fn download_file(Path(id): Path) -> Result, StatusCode> { +async fn upload_file( + Path(id): Path, + mut multipart: Multipart, +) -> 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) + } +} + +async fn download_file(Path(id): Path) -> Result { if !validate_id(id.as_str()) { return Err(StatusCode::BAD_REQUEST); } let mut path = get_dir(); path.push(id.clone()); - if let Ok(data) = tokio::fs::read(path).await { - Ok(data) - } else { - Err(StatusCode::NOT_FOUND) - } + + let file = tokio::fs::File::open(path).await.map_err(|_| StatusCode::NOT_FOUND)?; + let stream = ReaderStream::new(file); + let body = Body::from_stream(stream); + let response = Response::builder() + .header(CONTENT_TYPE, "application/octet-stream") // Or detect mime type + .status(StatusCode::OK).body(body) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(response) } async fn version(Path(id): Path) -> Result { if !validate_id(id.as_str()) { - return Err(StatusCode::BAD_REQUEST); + return Err(StatusCode::NOT_FOUND); } let dir = get_dir().join(id);