diff --git a/Cargo.lock b/Cargo.lock index ee66f07..fa5ea81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,6 +169,7 @@ dependencies = [ "matchit", "memchr", "mime", + "multer", "percent-encoding", "pin-project-lite", "serde_core", @@ -535,6 +536,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[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" @@ -1621,6 +1631,23 @@ dependencies = [ "windows-sys 0.59.0", ] +[[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 = "notify" version = "7.0.0" @@ -2531,6 +2558,12 @@ dependencies = [ "windows-sys 0.61.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" diff --git a/Cargo.toml b/Cargo.toml index e21876f..e28ca97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ thiserror = "2" open = "5" # Web server (axum + tower) -axum = { version = "0.8", features = ["ws"] } +axum = { version = "0.8", features = ["ws", "multipart"] } tower = "0.5" tower-http = { version = "0.6", features = ["cors", "fs", "trace"] } # Frontend embedding diff --git a/src/frontend/index.html b/src/frontend/index.html index 505290a..6c5532a 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -2,7 +2,7 @@ - + Imago Dashboard @@ -163,12 +251,16 @@ Déconnecté + + +
+
@@ -179,6 +271,14 @@
En ligne
●
+ +
+
📁
+
Glissez-déposez des images ici
+
ou cliquez pour sélectionner — PNG, JPG, GIF, WebP
+ +
+ @@ -194,6 +294,9 @@ + + + diff --git a/src/server/routes.rs b/src/server/routes.rs index 0f9a633..d478692 100644 --- a/src/server/routes.rs +++ b/src/server/routes.rs @@ -1,8 +1,8 @@ //! Routes API REST du dashboard — proxy vers Imago v2 use axum::{ - Router, routing::get, - extract::{Path, Query, State}, + Router, routing::{get, post}, + extract::{Multipart, Path, Query, State}, response::IntoResponse, Json, }; @@ -55,6 +55,7 @@ pub fn api_routes(state: AppState) -> Router { let state = Arc::new(state); Router::new() .route("/api/health", get(health_check)) + .route("/api/upload", post(upload_image)) .route("/api/images", get(list_images)) .route("/api/images/search", get(search_images)) .route("/api/images/{id}", get(get_image)) @@ -69,6 +70,65 @@ async fn health_check() -> &'static str { "OK" } +/// Upload d'image : reçoit un fichier et le transmet à l'API Imago +async fn upload_image( + State(state): State>, + mut multipart: Multipart, +) -> impl IntoResponse { + let mut data: Vec = Vec::new(); + let mut filename = String::from("upload.png"); + let mut mime_type = String::from("image/png"); + + while let Ok(Some(mut field)) = multipart.next_field().await { + let name = field.name().unwrap_or("").to_string(); + if name == "file" { + if let Some(fn_) = field.file_name() { + filename = fn_.to_string(); + } + if let Some(mt) = field.content_type() { + mime_type = mt.to_string(); + } + while let Ok(Some(chunk)) = field.chunk().await { + data.extend_from_slice(&chunk); + } + } + } + + if data.is_empty() { + return ( + axum::http::StatusCode::BAD_REQUEST, + Json(serde_json::json!({"error": "Aucun fichier reçu"})), + ); + } + + // Validation MIME basique côté dashboard + if !mime_type.starts_with("image/") { + return ( + axum::http::StatusCode::UNSUPPORTED_MEDIA_TYPE, + Json(serde_json::json!({"error": format!("Type MIME non supporté : {}", mime_type)})), + ); + } + + match state.imago.upload_image(data, &filename, None).await { + Ok(response) => ( + axum::http::StatusCode::CREATED, + Json(serde_json::json!({ + "id": response.id, + "uuid": response.uuid, + "filename": response.original_name, + "status": response.status, + })), + ), + Err(e) => { + tracing::error!("Upload vers Imago échoué : {:#}", e); + ( + axum::http::StatusCode::BAD_GATEWAY, + Json(serde_json::json!({"error": format!("Échec de l'upload : {:#}", e)})), + ) + } + } +} + async fn list_images( State(state): State>, Query(pagination): Query,