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:
2026-06-25 14:23:55 -04:00
parent 3254c33ae5
commit 5a2e12da01
7 changed files with 58 additions and 9 deletions
+34
View File
@@ -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,