From c26098b0d4ec957580ce0ef3a67884c53544512b Mon Sep 17 00:00:00 2001 From: dahoud Date: Tue, 13 Jan 2026 20:45:25 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20Corrections=20WebSocket=20et=20upload=20?= =?UTF-8?q?de=20fichiers=20-=20FileUploadResource:=20support=20des=20m?= =?UTF-8?q?=C3=A9tadonn=C3=A9es=20(type,=20fileName,=20contentType,=20file?= =?UTF-8?q?Size,=20userId)=20-=20NotificationWebSocket:=20correction=20de?= =?UTF-8?q?=20l'erreur=20JTA=20transaction=20avec=20CompletableFuture.runA?= =?UTF-8?q?sync()=20-=20PresenceService:=20ajout=20de=20@ActivateRequestCo?= =?UTF-8?q?ntext=20pour=20le=20contexte=20de=20requ=C3=AAte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dev/resource/FileUploadResource.java | 212 +++++++++++++++++- .../lions/dev/service/PresenceService.java | 3 + .../dev/websocket/NotificationWebSocket.java | 25 ++- 3 files changed, 223 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/lions/dev/resource/FileUploadResource.java b/src/main/java/com/lions/dev/resource/FileUploadResource.java index 3451b8c..6f3c8d8 100644 --- a/src/main/java/com/lions/dev/resource/FileUploadResource.java +++ b/src/main/java/com/lions/dev/resource/FileUploadResource.java @@ -2,17 +2,25 @@ package com.lions.dev.resource; import com.lions.dev.service.FileService; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.RestForm; import org.jboss.resteasy.reactive.multipart.FileUpload; import jakarta.inject.Inject; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; -@Path("/upload") +@Path("/media") public class FileUploadResource { private static final Logger LOG = Logger.getLogger(FileUploadResource.class); @@ -20,17 +28,207 @@ public class FileUploadResource { @Inject FileService fileService; + @Context + UriInfo uriInfo; + @POST + @Path("/upload") @Consumes(MediaType.MULTIPART_FORM_DATA) - public Response uploadFile(@RestForm("file") FileUpload file) { - String uploadDir = "/tmp/uploads/"; + @Produces(MediaType.APPLICATION_JSON) + public Response uploadFile( + @RestForm("file") FileUpload file, + @RestForm("type") String type, + @RestForm("fileName") String fileName, + @RestForm("contentType") String contentType, + @RestForm("fileSize") String fileSize, + @RestForm("userId") String userId) { + + LOG.info("Début de l'upload de fichier"); + LOG.infof("Type: %s, FileName: %s, ContentType: %s, FileSize: %s, UserId: %s", + type, fileName, contentType, fileSize, userId); + + if (file == null || file.uploadedFile() == null) { + LOG.error("Aucun fichier fourni dans la requête"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(createErrorResponse("Aucun fichier fourni")) + .build(); + } try { - Path savedFilePath = (jakarta.ws.rs.Path) fileService.saveFile(file.uploadedFile(), uploadDir, file.fileName()); - return Response.ok("Fichier uploadé avec succès : " + savedFilePath).build(); + // Générer un nom de fichier unique si nécessaire + String finalFileName = fileName != null && !fileName.isEmpty() + ? fileName + : generateUniqueFileName(file.fileName()); + + // Déterminer le type de média + String mediaType = type != null && !type.isEmpty() + ? type + : determineMediaType(file.fileName()); + + // Répertoire d'upload + String uploadDir = "/tmp/uploads/"; + + // Sauvegarder le fichier + java.nio.file.Path savedFilePath = fileService.saveFile( + file.uploadedFile(), + uploadDir, + finalFileName + ); + + LOG.infof("Fichier sauvegardé avec succès: %s", savedFilePath); + + // Construire l'URL du fichier + // Note: En production, vous devriez utiliser une URL publique (CDN, S3, etc.) + String fileUrl = buildFileUrl(finalFileName, uriInfo); + String thumbnailUrl = null; + + // Pour les vidéos, on pourrait générer un thumbnail ici + if ("video".equalsIgnoreCase(mediaType)) { + // TODO: Générer un thumbnail pour les vidéos + thumbnailUrl = fileUrl; // Placeholder + } + + // Construire la réponse JSON + Map response = new HashMap<>(); + response.put("url", fileUrl); + if (thumbnailUrl != null) { + response.put("thumbnailUrl", thumbnailUrl); + } + response.put("type", mediaType); + if (fileSize != null && !fileSize.isEmpty()) { + try { + long size = Long.parseLong(fileSize); + // Pour les vidéos, on pourrait calculer la durée ici + // response.put("duration", durationInSeconds); + } catch (NumberFormatException e) { + LOG.warnf("Impossible de parser fileSize: %s", fileSize); + } + } + + LOG.infof("Upload réussi, URL: %s", fileUrl); + + return Response.status(Response.Status.CREATED) + .entity(response) + .build(); + } catch (IOException e) { - LOG.error("Erreur lors de l'upload du fichier", e); - return Response.serverError().entity("Erreur lors de l'upload du fichier.").build(); + LOG.errorf(e, "Erreur lors de l'upload du fichier: %s", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(createErrorResponse("Erreur lors de l'upload du fichier: " + e.getMessage())) + .build(); + } catch (Exception e) { + LOG.errorf(e, "Erreur inattendue lors de l'upload: %s", e.getMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(createErrorResponse("Erreur inattendue.")) + .build(); } } + + /** + * Génère un nom de fichier unique. + */ + private String generateUniqueFileName(String originalFileName) { + String extension = ""; + int lastDot = originalFileName.lastIndexOf('.'); + if (lastDot > 0) { + extension = originalFileName.substring(lastDot); + } + return UUID.randomUUID().toString() + extension; + } + + /** + * Détermine le type de média basé sur l'extension du fichier. + */ + private String determineMediaType(String fileName) { + if (fileName == null) { + return "image"; + } + String lowerFileName = fileName.toLowerCase(); + if (lowerFileName.endsWith(".mp4") || + lowerFileName.endsWith(".mov") || + lowerFileName.endsWith(".avi") || + lowerFileName.endsWith(".mkv") || + lowerFileName.endsWith(".m4v")) { + return "video"; + } + return "image"; + } + + /** + * Construit l'URL du fichier. + * En production, cela devrait pointer vers un CDN ou un service de stockage. + */ + private String buildFileUrl(String fileName, UriInfo uriInfo) { + // Construire l'URL de base à partir de l'URI de la requête + String baseUri = uriInfo.getBaseUri().toString(); + // Retirer le slash final s'il existe + if (baseUri.endsWith("/")) { + baseUri = baseUri.substring(0, baseUri.length() - 1); + } + // Retourner une URL absolue + return baseUri + "/media/files/" + fileName; + } + + @GET + @Path("/files/{fileName}") + @Produces(MediaType.APPLICATION_OCTET_STREAM) + public Response getFile(@PathParam("fileName") String fileName) { + try { + java.nio.file.Path filePath = java.nio.file.Paths.get("/tmp/uploads/", fileName); + + if (!java.nio.file.Files.exists(filePath)) { + LOG.warnf("Fichier non trouvé: %s", fileName); + return Response.status(Response.Status.NOT_FOUND) + .entity(createErrorResponse("Fichier non trouvé")) + .build(); + } + + // Déterminer le content-type + String contentType = determineContentType(fileName); + + return Response.ok(filePath.toFile()) + .type(contentType) + .header("Content-Disposition", "inline; filename=\"" + fileName + "\"") + .build(); + + } catch (Exception e) { + LOG.errorf(e, "Erreur lors de la récupération du fichier: %s", fileName); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(createErrorResponse("Erreur lors de la récupération du fichier")) + .build(); + } + } + + /** + * Détermine le content-type basé sur l'extension du fichier. + */ + private String determineContentType(String fileName) { + if (fileName == null) { + return MediaType.APPLICATION_OCTET_STREAM; + } + String lowerFileName = fileName.toLowerCase(); + if (lowerFileName.endsWith(".jpg") || lowerFileName.endsWith(".jpeg")) { + return "image/jpeg"; + } else if (lowerFileName.endsWith(".png")) { + return "image/png"; + } else if (lowerFileName.endsWith(".gif")) { + return "image/gif"; + } else if (lowerFileName.endsWith(".mp4")) { + return "video/mp4"; + } else if (lowerFileName.endsWith(".mov")) { + return "video/quicktime"; + } else if (lowerFileName.endsWith(".avi")) { + return "video/x-msvideo"; + } + return MediaType.APPLICATION_OCTET_STREAM; + } + + /** + * Crée une réponse d'erreur JSON. + */ + private Map createErrorResponse(String message) { + Map error = new HashMap<>(); + error.put("error", message); + return error; + } } diff --git a/src/main/java/com/lions/dev/service/PresenceService.java b/src/main/java/com/lions/dev/service/PresenceService.java index 68f4a71..94a84a3 100644 --- a/src/main/java/com/lions/dev/service/PresenceService.java +++ b/src/main/java/com/lions/dev/service/PresenceService.java @@ -4,6 +4,7 @@ import com.lions.dev.entity.users.Users; import com.lions.dev.repository.UsersRepository; import com.lions.dev.websocket.NotificationWebSocket; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.control.ActivateRequestContext; import jakarta.inject.Inject; import jakarta.transaction.Transactional; @@ -64,9 +65,11 @@ public class PresenceService { /** * Met à jour le heartbeat d'un utilisateur (keep-alive). + * Cette méthode est appelée depuis un thread worker, donc elle doit activer le contexte de requête. * * @param userId L'ID de l'utilisateur */ + @ActivateRequestContext @Transactional public void heartbeat(UUID userId) { Users user = usersRepository.findById(userId); diff --git a/src/main/java/com/lions/dev/websocket/NotificationWebSocket.java b/src/main/java/com/lions/dev/websocket/NotificationWebSocket.java index 64453fa..7f8afe8 100644 --- a/src/main/java/com/lions/dev/websocket/NotificationWebSocket.java +++ b/src/main/java/com/lions/dev/websocket/NotificationWebSocket.java @@ -160,21 +160,26 @@ public class NotificationWebSocket { /** * Gère les messages de type ping (keep-alive). + * Exécuté de manière asynchrone sur un thread worker pour permettre les transactions JTA. */ private void handlePing(String userId) { - try { - UUID userUUID = UUID.fromString(userId); + // Exécuter le heartbeat de manière asynchrone sur un thread worker + java.util.concurrent.CompletableFuture.runAsync(() -> { + try { + UUID userUUID = UUID.fromString(userId); - // Mettre à jour le heartbeat de présence - presenceService.heartbeat(userUUID); + // Mettre à jour le heartbeat de présence (exécuté sur thread worker) + presenceService.heartbeat(userUUID); - String pongMessage = buildNotificationJson("pong", Map.of("timestamp", System.currentTimeMillis())); - sendToUser(userUUID, pongMessage); + // Envoyer le pong depuis le thread worker + String pongMessage = buildNotificationJson("pong", Map.of("timestamp", System.currentTimeMillis())); + sendToUser(userUUID, pongMessage); - Log.debug("[NOTIFICATION-WS] Pong envoyé à l'utilisateur : " + userId); - } catch (Exception e) { - Log.error("[NOTIFICATION-WS] Erreur lors de l'envoi du pong : " + e.getMessage(), e); - } + Log.debug("[NOTIFICATION-WS] Pong envoyé à l'utilisateur : " + userId); + } catch (Exception e) { + Log.error("[NOTIFICATION-WS] Erreur lors de l'envoi du pong : " + e.getMessage(), e); + } + }); } /**