diff --git a/Makefile b/Makefile index 2e05463..c6270fb 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: docker-test-perf build up clean compile update test +.PHONY: docker-test-perf build up clean compile update test test-duplicate build: docker-compose build @@ -22,5 +22,8 @@ update: docker-test-perf: docker-compose exec app mvn test -f /source/pom.xml -Dtest=ChunkingPerformanceTest +test-duplicate: + docker-compose exec app mvn test -f /source/pom.xml -Dtest=DuplicationPerformanceTest + test: docker-compose exec app mvn test -f /source/pom.xml \ No newline at end of file diff --git a/java/src/main/java/com/goofy/GoofyFiles/controller/api/DuplicationController.java b/java/src/main/java/com/goofy/GoofyFiles/controller/api/DuplicationController.java index 72b2b70..40f2caa 100644 --- a/java/src/main/java/com/goofy/GoofyFiles/controller/api/DuplicationController.java +++ b/java/src/main/java/com/goofy/GoofyFiles/controller/api/DuplicationController.java @@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import com.goofy.GoofyFiles.compression.CompressionService; import com.goofy.GoofyFiles.duplication.DuplicationService; import com.goofy.GoofyFiles.duplication.HashingAlgorithm; @@ -63,4 +64,28 @@ public ResponseEntity processFile( .body(Map.of("error", "Échec du traitement et de l'enregistrement du fichier: " + e.getMessage())); } } -} \ No newline at end of file + + @PostMapping("/process-compressed") + public ResponseEntity processFileCompressed( + @RequestParam("file") MultipartFile file, + @RequestParam(value = "algorithm", defaultValue = "SHA256") HashingAlgorithm algorithm, + @RequestParam(value = "compression", defaultValue = "LZ4") CompressionService.CompressionType compression) { + try { + File tempFile = File.createTempFile("upload-", "-" + file.getOriginalFilename()); + file.transferTo(tempFile); + + Map result = duplicationService.processAndStoreFileCompressed( + tempFile, + file.getOriginalFilename(), + file.getSize(), + algorithm, + compression); + + tempFile.delete(); + return ResponseEntity.ok(result); + } catch (IOException e) { + return ResponseEntity.internalServerError() + .body(Map.of("error", "Échec du traitement et de l'enregistrement du fichier compressé: " + e.getMessage())); + } + } +} diff --git a/java/src/main/java/com/goofy/GoofyFiles/duplication/DuplicationService.java b/java/src/main/java/com/goofy/GoofyFiles/duplication/DuplicationService.java index b9b44b4..3dbd313 100644 --- a/java/src/main/java/com/goofy/GoofyFiles/duplication/DuplicationService.java +++ b/java/src/main/java/com/goofy/GoofyFiles/duplication/DuplicationService.java @@ -18,6 +18,8 @@ import com.goofy.GoofyFiles.chunking.Chunk; import com.goofy.GoofyFiles.chunking.ChunkingService; +import com.goofy.GoofyFiles.compression.CompressionService; +import com.goofy.GoofyFiles.compression.CompressionService.CompressionType; import com.goofy.GoofyFiles.model.ChunkEntity; import com.goofy.GoofyFiles.model.FileChunkEntity; import com.goofy.GoofyFiles.model.FileEntity; @@ -35,6 +37,7 @@ public class DuplicationService { private final FileRepository fileRepository; private final ChunkRepository chunkRepository; private final FileChunkRepository fileChunkRepository; + private final CompressionService compressionService; /** * Constructeur principal pour l'utilisation en production @@ -44,11 +47,13 @@ public DuplicationService( ChunkingService chunkingService, FileRepository fileRepository, ChunkRepository chunkRepository, - FileChunkRepository fileChunkRepository) { + FileChunkRepository fileChunkRepository, + CompressionService compressionService) { this.chunkingService = chunkingService; this.fileRepository = fileRepository; this.chunkRepository = chunkRepository; this.fileChunkRepository = fileChunkRepository; + this.compressionService = compressionService; } /** @@ -57,10 +62,7 @@ public DuplicationService( * pas disponibles */ public DuplicationService(ChunkingService chunkingService) { - this.chunkingService = chunkingService; - this.fileRepository = null; - this.chunkRepository = null; - this.fileChunkRepository = null; + this(chunkingService, null, null, null, null); } public Map analyzeFile(File file, HashingAlgorithm algorithm) throws IOException { @@ -74,8 +76,6 @@ public Map analyzeFile(File file, HashingAlgorithm algorithm) th chunk.getPosition(), chunk.getData().length, hash); } - // Filtrer les chunks qui apparaissent plus d'une fois (vous pouvez logguer ou - // utiliser ce résultat) duplicates.entrySet().stream() .filter(e -> e.getValue() > 1); @@ -106,7 +106,6 @@ private String calculateHash(byte[] data, HashingAlgorithm algorithm) { case SHA256: return Hashing.sha256().hashBytes(data).toString(); case BLAKE3: - // Utilisation de Apache Commons Codec pour BLAKE3 byte[] hashBytes = Blake3.hash(data); return Hex.encodeHexString(hashBytes); default: @@ -174,7 +173,6 @@ public Map processAndStoreFile( existingChunk = Optional.empty(); } - // Traiter le chunk (nouveau ou existant) ChunkEntity chunkEntity; if (existingChunk.isPresent()) { chunkEntity = existingChunk.get(); @@ -228,4 +226,128 @@ public Map processAndStoreFile( return result; } -} \ No newline at end of file + + @Transactional + public Map processAndStoreFileCompressed( + File file, + String fileName, + long fileSize, + HashingAlgorithm algorithm, + CompressionType compressionType) throws IOException { + if (fileRepository == null || chunkRepository == null || fileChunkRepository == null + || compressionService == null) { + throw new UnsupportedOperationException( + "Cette méthode nécessite les repositories et le service de compression qui n'ont pas été injectés. " + + "Utilisez le constructeur avec tous les paramètres pour cette fonctionnalité."); + } + + // 1. Extraire le nom et l'extension + String name = fileName; + String extension = ""; + int lastDotIndex = fileName.lastIndexOf('.'); + if (lastDotIndex > 0) { + name = fileName.substring(0, lastDotIndex); + extension = fileName.substring(lastDotIndex + 1); + } + + // 2. Créer et sauvegarder l'entité de fichier + FileEntity fileEntity = new FileEntity(); + fileEntity.setName(name); + fileEntity.setExtension(extension); + fileEntity.setSize(fileSize); + fileEntity = fileRepository.save(fileEntity); + + // 3. Découper le fichier + List chunks = chunkingService.chunkFile(file); + + // Statistiques pour le résultat + int totalChunks = chunks.size(); + int duplicateChunks = 0; + int uniqueChunks = 0; + long savedStorage = 0; + long totalCompressedSize = 0; + + // 4. Traiter chaque chunk + for (Chunk chunk : chunks) { + String hash = calculateHash(chunk.getData(), algorithm); + + // Chercher si ce chunk existe déjà en base + Optional existingChunk; + switch (algorithm) { + case SHA1: + existingChunk = chunkRepository.findByHashSha1(hash); + break; + case SHA256: + existingChunk = chunkRepository.findByHashSha256(hash); + break; + case BLAKE3: + existingChunk = chunkRepository.findByHashBlake3(hash); + break; + default: + existingChunk = Optional.empty(); + } + + ChunkEntity chunkEntity; + if (existingChunk.isPresent()) { + chunkEntity = existingChunk.get(); + duplicateChunks++; + savedStorage += chunk.getOriginalSize(); + logger.info("Chunk dupliqué trouvé: {}", hash); + } else { + // Compression du chunk + byte[] compressedData = compressionService.compress(chunk.getData(), compressionType); + totalCompressedSize += compressedData.length; + + chunkEntity = new ChunkEntity(); + // Stocker les données compressées + chunkEntity.setData(compressedData); + // Vous pouvez ajouter une propriété pour stocker la taille originale si besoin, + // ex : + // chunkEntity.setOriginalSize(chunk.getData().length); + + // Stocker le hash selon l'algorithme + switch (algorithm) { + case SHA1: + chunkEntity.setHashSha1(hash); + break; + case SHA256: + chunkEntity.setHashSha256(hash); + break; + case BLAKE3: + chunkEntity.setHashBlake3(hash); + break; + } + + chunkEntity = chunkRepository.save(chunkEntity); + uniqueChunks++; + } + + // Créer la relation entre le fichier et le chunk + FileChunkEntity fileChunk = new FileChunkEntity(); + fileChunk.setFile(fileEntity); + fileChunk.setChunk(chunkEntity); + fileChunk.setPosition(chunk.getPosition()); + fileChunkRepository.save(fileChunk); + } + + // 5. Préparer le résultat + Map result = new HashMap<>(); + result.put("fileId", fileEntity.getId()); + result.put("fileName", fileEntity.getName()); + result.put("extension", fileEntity.getExtension()); + result.put("fileSize", fileEntity.getSize()); + result.put("algorithm", algorithm.name()); + result.put("compressionType", compressionType.name()); + result.put("totalChunks", totalChunks); + result.put("uniqueChunks", uniqueChunks); + result.put("duplicateChunks", duplicateChunks); + result.put("savedStorage", savedStorage); + result.put("deduplicationRatio", totalChunks > 0 ? (double) duplicateChunks / totalChunks : 0); + result.put("totalCompressedSize", totalCompressedSize); + + logger.info("Fichier compressé traité: id={}, nom={}, chunks={}, uniques={}, doublons={}, taille compressée={}", + fileEntity.getId(), fileName, totalChunks, uniqueChunks, duplicateChunks, totalCompressedSize); + + return result; + } +} diff --git a/java/src/test/java/com/goofy/GoofyFiles/duplication/DuplicationPerformanceTest.java b/java/src/test/java/com/goofy/GoofyFiles/duplication/DuplicationPerformanceTest.java index 4db7bdc..c47078b 100644 --- a/java/src/test/java/com/goofy/GoofyFiles/duplication/DuplicationPerformanceTest.java +++ b/java/src/test/java/com/goofy/GoofyFiles/duplication/DuplicationPerformanceTest.java @@ -1,18 +1,36 @@ package com.goofy.GoofyFiles.duplication; -import com.goofy.GoofyFiles.chunking.ChunkingService; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Path; import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Random; -import static org.junit.jupiter.api.Assertions.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.goofy.GoofyFiles.chunking.Chunk; +import com.goofy.GoofyFiles.chunking.ChunkingService; +import com.goofy.GoofyFiles.compression.CompressionService; +import com.goofy.GoofyFiles.model.ChunkEntity; +import com.goofy.GoofyFiles.model.FileChunkEntity; +import com.goofy.GoofyFiles.model.FileEntity; +import com.goofy.GoofyFiles.repository.ChunkRepository; +import com.goofy.GoofyFiles.repository.FileChunkRepository; +import com.goofy.GoofyFiles.repository.FileRepository; class DuplicationPerformanceTest { @@ -22,6 +40,7 @@ class DuplicationPerformanceTest { @BeforeEach void setUp() throws NoSuchAlgorithmException { chunkingService = new ChunkingService(); + // Pour le test d'analyse, on utilise le constructeur simplifié duplicationService = new DuplicationService(chunkingService); } @@ -50,24 +69,25 @@ void testDuplicationDetectionWithDifferentAlgorithms(@TempDir Path tempDir) thro System.out.println("SHA-1:"); System.out.println(" - Temps d'exécution: " + sha1Time / 1_000_000.0 + " ms"); System.out.println(" - Chunks uniques: " + sha1Results.get("uniqueChunks")); - System.out.println(" - Chunks dupliqués: " + sha1Results.get("duplicatedChunks")); + System.out.println(" - Chunks dupliqués: " + sha1Results.get("duplicateChunks")); System.out.println(" - Détails des doublons: " + sha1Results.get("duplicateDetails")); System.out.println("\nSHA-256:"); System.out.println(" - Temps d'exécution: " + sha256Time / 1_000_000.0 + " ms"); System.out.println(" - Chunks uniques: " + sha256Results.get("uniqueChunks")); - System.out.println(" - Chunks dupliqués: " + sha256Results.get("duplicatedChunks")); + System.out.println(" - Chunks dupliqués: " + sha256Results.get("duplicateChunks")); System.out.println(" - Détails des doublons: " + sha256Results.get("duplicateDetails")); System.out.println("\nBLAKE3:"); System.out.println(" - Temps d'exécution: " + blake3Time / 1_000_000.0 + " ms"); System.out.println(" - Chunks uniques: " + blake3Results.get("uniqueChunks")); - System.out.println(" - Chunks dupliqués: " + blake3Results.get("duplicatedChunks")); + System.out.println(" - Chunks dupliqués: " + blake3Results.get("duplicateChunks")); System.out.println(" - Détails des doublons: " + blake3Results.get("duplicateDetails")); // Vérifications assertTrue((Long) sha1Results.get("duplicatedChunks") > 0, "Des doublons devraient être détectés pour SHA-1"); - assertTrue((Long) blake3Results.get("duplicatedChunks") > 0, "Des doublons devraient être détectés pour BLAKE3"); + assertTrue((Long) blake3Results.get("duplicatedChunks") > 0, + "Des doublons devraient être détectés pour BLAKE3"); // Le nombre de chunks uniques doit être identique pour tous les algorithmes assertEquals(sha1Results.get("uniqueChunks"), sha256Results.get("uniqueChunks"), "Le nombre de chunks uniques doit être le même pour SHA-1 et SHA-256"); @@ -75,6 +95,66 @@ void testDuplicationDetectionWithDifferentAlgorithms(@TempDir Path tempDir) thro "Le nombre de chunks uniques doit être le même pour SHA-1 et BLAKE3"); } + @Test + void testProcessAndStoreFileCompressed(@TempDir Path tempDir) throws IOException { + // Créer un fichier de test avec des données répétitives (1MB) + File testFile = createTestFile(tempDir, 1024 * 1024); + + // Utilisation de mocks pour les repositories + FileRepository fileRepo = mock(FileRepository.class); + when(fileRepo.save(any(FileEntity.class))).thenAnswer(invocation -> { + FileEntity entity = invocation.getArgument(0); + entity.setId(1L); + return entity; + }); + + ChunkRepository chunkRepo = mock(ChunkRepository.class); + // Par défaut, aucun chunk n'est trouvé (pour simuler des chunks nouveaux) + when(chunkRepo.findByHashSha1(anyString())).thenReturn(Optional.empty()); + when(chunkRepo.findByHashSha256(anyString())).thenReturn(Optional.empty()); + when(chunkRepo.findByHashBlake3(anyString())).thenReturn(Optional.empty()); + when(chunkRepo.save(any(ChunkEntity.class))).thenAnswer(invocation -> { + ChunkEntity entity = invocation.getArgument(0); + entity.setId(1L); + return entity; + }); + + FileChunkRepository fileChunkRepo = mock(FileChunkRepository.class); + when(fileChunkRepo.save(any(FileChunkEntity.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + CompressionService compressionService = new CompressionService(); + + // Ré-instancier le service avec toutes les dépendances + duplicationService = new DuplicationService(chunkingService, fileRepo, chunkRepo, fileChunkRepo, + compressionService); + + Map result = duplicationService.processAndStoreFileCompressed( + testFile, + testFile.getName(), + testFile.length(), + HashingAlgorithm.SHA256, + CompressionService.CompressionType.LZ4); + + // Vérifier que le résultat contient les clés attendues + assertNotNull(result.get("fileId")); + assertNotNull(result.get("fileName")); + assertNotNull(result.get("totalChunks")); + assertNotNull(result.get("uniqueChunks")); + assertNotNull(result.get("duplicateChunks")); + assertNotNull(result.get("totalCompressedSize")); + + // Vérifier que la taille compressée totale est inférieure à la somme des + // tailles originales + List chunks = chunkingService.chunkFile(testFile); + long totalOriginalSize = chunks.stream().mapToLong(chunk -> chunk.getData().length).sum(); + long totalCompressedSize = ((Number) result.get("totalCompressedSize")).longValue(); + assertTrue(totalCompressedSize < totalOriginalSize, + "La taille compressée (" + totalCompressedSize + " octets) doit être inférieure à la taille originale (" + + totalOriginalSize + " octets)"); + + System.out.println("ProcessAndStoreFileCompressed result: " + result); + } + private File createTestFile(Path tempDir, int size) throws IOException { File file = tempDir.resolve("test.dat").toFile(); try (FileOutputStream fos = new FileOutputStream(file)) { @@ -82,7 +162,7 @@ private File createTestFile(Path tempDir, int size) throws IOException { byte[][] patterns = new byte[4][]; for (int i = 0; i < patterns.length; i++) { patterns[i] = new byte[8192]; // 8KB par pattern - Arrays.fill(patterns[i], (byte)i); + Arrays.fill(patterns[i], (byte) i); } // Écrire les patterns de manière répétitive Random random = new Random(42);