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 com.lions.dev.service.FileService;
|
||||||
import jakarta.ws.rs.Consumes;
|
import jakarta.ws.rs.Consumes;
|
||||||
|
import jakarta.ws.rs.GET;
|
||||||
import jakarta.ws.rs.POST;
|
import jakarta.ws.rs.POST;
|
||||||
import jakarta.ws.rs.Path;
|
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.MediaType;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
|
import jakarta.ws.rs.core.UriInfo;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.reactive.RestForm;
|
import org.jboss.resteasy.reactive.RestForm;
|
||||||
import org.jboss.resteasy.reactive.multipart.FileUpload;
|
import org.jboss.resteasy.reactive.multipart.FileUpload;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Path("/upload")
|
@Path("/media")
|
||||||
public class FileUploadResource {
|
public class FileUploadResource {
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(FileUploadResource.class);
|
private static final Logger LOG = Logger.getLogger(FileUploadResource.class);
|
||||||
@@ -20,17 +28,207 @@ public class FileUploadResource {
|
|||||||
@Inject
|
@Inject
|
||||||
FileService fileService;
|
FileService fileService;
|
||||||
|
|
||||||
|
@Context
|
||||||
|
UriInfo uriInfo;
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
|
@Path("/upload")
|
||||||
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
@Consumes(MediaType.MULTIPART_FORM_DATA)
|
||||||
public Response uploadFile(@RestForm("file") FileUpload file) {
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
String uploadDir = "/tmp/uploads/";
|
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 {
|
try {
|
||||||
Path savedFilePath = (jakarta.ws.rs.Path) fileService.saveFile(file.uploadedFile(), uploadDir, file.fileName());
|
// Générer un nom de fichier unique si nécessaire
|
||||||
return Response.ok("Fichier uploadé avec succès : " + savedFilePath).build();
|
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) {
|
} catch (IOException e) {
|
||||||
LOG.error("Erreur lors de l'upload du fichier", e);
|
LOG.errorf(e, "Erreur lors de l'upload du fichier: %s", e.getMessage());
|
||||||
return Response.serverError().entity("Erreur lors de l'upload du fichier.").build();
|
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.repository.UsersRepository;
|
||||||
import com.lions.dev.websocket.NotificationWebSocket;
|
import com.lions.dev.websocket.NotificationWebSocket;
|
||||||
import jakarta.enterprise.context.ApplicationScoped;
|
import jakarta.enterprise.context.ApplicationScoped;
|
||||||
|
import jakarta.enterprise.context.control.ActivateRequestContext;
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
|
|
||||||
@@ -64,9 +65,11 @@ public class PresenceService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Met à jour le heartbeat d'un utilisateur (keep-alive).
|
* 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
|
* @param userId L'ID de l'utilisateur
|
||||||
*/
|
*/
|
||||||
|
@ActivateRequestContext
|
||||||
@Transactional
|
@Transactional
|
||||||
public void heartbeat(UUID userId) {
|
public void heartbeat(UUID userId) {
|
||||||
Users user = usersRepository.findById(userId);
|
Users user = usersRepository.findById(userId);
|
||||||
|
|||||||
@@ -160,21 +160,26 @@ public class NotificationWebSocket {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gère les messages de type ping (keep-alive).
|
* 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) {
|
private void handlePing(String userId) {
|
||||||
try {
|
// Exécuter le heartbeat de manière asynchrone sur un thread worker
|
||||||
UUID userUUID = UUID.fromString(userId);
|
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);
|
presenceService.heartbeat(userUUID);
|
||||||
|
|
||||||
String pongMessage = buildNotificationJson("pong", Map.of("timestamp", System.currentTimeMillis()));
|
// Envoyer le pong depuis le thread worker
|
||||||
sendToUser(userUUID, pongMessage);
|
String pongMessage = buildNotificationJson("pong", Map.of("timestamp", System.currentTimeMillis()));
|
||||||
|
sendToUser(userUUID, pongMessage);
|
||||||
|
|
||||||
Log.debug("[NOTIFICATION-WS] Pong envoyé à l'utilisateur : " + userId);
|
Log.debug("[NOTIFICATION-WS] Pong envoyé à l'utilisateur : " + userId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.error("[NOTIFICATION-WS] Erreur lors de l'envoi du pong : " + e.getMessage(), e);
|
Log.error("[NOTIFICATION-WS] Erreur lors de l'envoi du pong : " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user