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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user