fix: anti-doublon upload — lock mémoire in-flight + garde click/drag frontend
- Backend: Mutex<HashSet> pour bloquer les uploads concurrents du même hash (409 Conflict) - Backend: logs tracing pour tracer chaque upload entrant - Frontend: flag isDragging pour éviter le double déclenchement click+drag - Dépendance: ajout hex = 0.4 pour log du hash Corrige le bug où un seul fichier uploadé générait 4 copies sur Imago.
This commit is contained in:
@@ -10,6 +10,8 @@ use axum::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use std::path::PathBuf;
|
||||
use std::collections::HashSet;
|
||||
use tokio::sync::Mutex;
|
||||
use crate::imago_client::{ImagoClient, ImageSummary};
|
||||
|
||||
/// État partagé de l'application
|
||||
@@ -20,6 +22,8 @@ pub struct AppState {
|
||||
pub ws_hub: crate::server::ws::WsHub,
|
||||
/// Chemin vers la DB de dédoublonnage (partagée avec le watcher)
|
||||
pub dedup_db_path: PathBuf,
|
||||
/// Uploads en cours — empêche les doublons concurrents
|
||||
pub in_flight: Mutex<HashSet<Vec<u8>>>,
|
||||
}
|
||||
|
||||
/// Crée un ImagoClient — utilise la clé fournie par l'utilisateur si présente,
|
||||
@@ -223,6 +227,29 @@ async fn upload_image(
|
||||
// Calculer le hash AVANT de consommer `data` (upload_image prend ownership)
|
||||
let file_hash = crate::dedup::Dedup::hash_bytes(&data);
|
||||
|
||||
// ═══ Garde anti-doublon : vérifier qu'aucun upload concurrent n'est en cours pour ce hash ═══
|
||||
{
|
||||
let mut in_flight = state.in_flight.lock().await;
|
||||
if in_flight.contains(&file_hash) {
|
||||
tracing::warn!(
|
||||
"🛑 Upload concurrent détecté pour {} ({} octets) — requête rejetée",
|
||||
filename, data.len()
|
||||
);
|
||||
return (
|
||||
axum::http::StatusCode::CONFLICT,
|
||||
Json(serde_json::json!({"error": "Upload déjà en cours pour ce fichier", "duplicate": true})),
|
||||
);
|
||||
}
|
||||
in_flight.insert(file_hash.clone());
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"📤 Upload reçu : {} ({} octets, hash: {}...)",
|
||||
filename,
|
||||
data.len(),
|
||||
hex::encode(&file_hash[..8.min(file_hash.len())])
|
||||
);
|
||||
|
||||
match client.upload_image(data, &filename, None).await {
|
||||
Ok(response) => {
|
||||
// Marquer le hash dans la DB de dédoublonnage pour que le watcher
|
||||
@@ -230,6 +257,10 @@ async fn upload_image(
|
||||
if let Ok(dedup) = crate::dedup::Dedup::open(&state.dedup_db_path, 1000) {
|
||||
let _ = dedup.mark_seen(&file_hash, &response.id.to_string());
|
||||
}
|
||||
|
||||
// Nettoyer le lock in-flight
|
||||
state.in_flight.lock().await.remove(&file_hash);
|
||||
|
||||
(
|
||||
axum::http::StatusCode::CREATED,
|
||||
Json(serde_json::json!({
|
||||
@@ -241,6 +272,9 @@ async fn upload_image(
|
||||
)
|
||||
}
|
||||
Err(e) => {
|
||||
// Nettoyer le lock in-flight même en cas d'échec
|
||||
state.in_flight.lock().await.remove(&file_hash);
|
||||
|
||||
tracing::error!("Upload vers Imago échoué : {:#}", e);
|
||||
(
|
||||
axum::http::StatusCode::BAD_GATEWAY,
|
||||
|
||||
Reference in New Issue
Block a user