fix: Corrections WebSocket et upload de fichiers - FileUploadResource: support des métadonnées (type, fileName, contentType, fileSize, userId) - NotificationWebSocket: correction de l'erreur JTA transaction avec CompletableFuture.runAsync() - PresenceService: ajout de @ActivateRequestContext pour le contexte de requête

This commit is contained in:
dahoud
2026-01-13 20:45:25 +00:00
parent bfb174bcf8
commit c26098b0d4
3 changed files with 223 additions and 17 deletions

View File

@@ -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<String, Object> 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<String, String> createErrorResponse(String message) {
Map<String, String> error = new HashMap<>();
error.put("error", message);
return error;
}
}

View File

@@ -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);

View File

@@ -160,14 +160,18 @@ 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) {
// 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
// Mettre à jour le heartbeat de présence (exécuté sur thread worker)
presenceService.heartbeat(userUUID);
// Envoyer le pong depuis le thread worker
String pongMessage = buildNotificationJson("pong", Map.of("timestamp", System.currentTimeMillis()));
sendToUser(userUUID, pongMessage);
@@ -175,6 +179,7 @@ public class NotificationWebSocket {
} catch (Exception e) {
Log.error("[NOTIFICATION-WS] Erreur lors de l'envoi du pong : " + e.getMessage(), e);
}
});
}
/**