Traitement d'Archives ZIP/RAR et Stockage d'Images en Base de Données avec Java Spring

L'intégration de fonctionnalités de téléversement de fichiers complexes dans une application d'entreprise nécessite une gestion rigoureuse des flux de données, de la décompression d'archives et de la manipulation d'images. Cet article détaille une architecture moderne permettant de réceptionner des archives compressées (ZIP et RAR), d'en extraire le contenu, de générer dynamiquement des miniatures, de persister les données binaires dans une base de données relationnelle via MyBatis, et enfin d'exposer ces ressources via une interface web Spring MVC.

Architecture et Modèles de Données

Le système s'appuie sur deux entités principales : CatalogItem pour représenter les catégories de produits, et MediaAsset pour stocker les fichiers binaires des images extraites. Les noms de variables et la structure ont été modernisés pour refléter une gestion générique d'actifs numériques.


package fr.tech.gallery.model;

public class CatalogItem {
    private String category;
    private String subCategory;
    private String collection;
    private String thumbnailRef;
    private String archiveRef;

    public CatalogItem() {}

    public CatalogItem(String category, String subCategory, String collection, String thumbnailRef, String archiveRef) {
        this.category = category;
        this.subCategory = subCategory;
        this.collection = collection;
        this.thumbnailRef = thumbnailRef;
        this.archiveRef = archiveRef;
    }
    
    // Getters et Setters omis pour la brièveté
}


package fr.tech.gallery.model;

public class MediaAsset {
    private String archiveRef;
    private boolean isThumbnail;
    private String fileName;
    private byte[] content;

    public MediaAsset() {}

    public MediaAsset(String archiveRef, boolean isThumbnail, String fileName, byte[] content) {
        this.archiveRef = archiveRef;
        this.isThumbnail = isThumbnail;
        this.fileName = fileName;
        this.content = content;
    }

    // Getters et Setters omis pour la brièveté
}

Utilitaires de Gestion des Fichiers et d'Images

Pour éviter les fuites de mémoire et les blocages de fichiers, la gestion des fichiers temporaires utilise l'API NIO de Java. La génération de miniatures remplace les anciennes méthodes de dessin par une approche Graphics2D plus performante et précise.


package fr.tech.gallery.util;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

public class StorageManager {

    private static final Path TEMP_DIR;

    static {
        try {
            TEMP_DIR = Files.createTempDirectory("gallery_uploads");
        } catch (IOException e) {
            throw new RuntimeException("Impossible d'initialiser le répertoire temporaire", e);
        }
    }

    public static Path saveTempFile(InputStream inputStream, String fileName) throws IOException {
        Path targetPath = TEMP_DIR.resolve(fileName);
        Files.copy(inputStream, targetPath, StandardCopyOption.REPLACE_EXISTING);
        return targetPath;
    }

    public static void cleanup(Path filePath) {
        try {
            if (filePath != null && Files.exists(filePath)) {
                Files.delete(filePath);
            }
        } catch (IOException e) {
            System.err.println("Échec de la suppression du fichier temporaire : " + filePath);
        }
    }
}


package fr.tech.gallery.util;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class ThumbnailGenerator {

    public static String generateThumbName(String originalName) {
        int dotIndex = originalName.lastIndexOf('.');
        if (dotIndex == -1) return originalName + "_thumb";
        return originalName.substring(0, dotIndex) + "_thumb" + originalName.substring(dotIndex);
    }

    public static byte[] resizeImage(InputStream source, int targetWidth, int targetHeight, String format) throws IOException {
        BufferedImage originalImage = ImageIO.read(source);
        if (originalImage == null) throw new IOException("Format d'image non supporté ou fichier corrompu.");

        BufferedImage resizedImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
        Graphics2D graphics = resizedImage.createGraphics();
        
        graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        graphics.drawImage(originalImage, 0, 0, targetWidth, targetHeight, null);
        graphics.dispose();

        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            ImageIO.write(resizedImage, format, outputStream);
            return outputStream.toByteArray();
        }
    }
}

Couche de Persistance avec MyBatis

L'interface du mapper et son fichier XML associé définissent les opérations CRUD nécessaires pour lier les éléments du catalogue à leurs actifs multimédias respectifs.


package fr.tech.gallery.repository;

import fr.tech.gallery.model.CatalogItem;
import fr.tech.gallery.model.MediaAsset;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface CatalogRepository {
    int insertCatalogItem(CatalogItem item);
    int saveMediaAsset(MediaAsset asset);
    List<mediaasset> findAssetsByCategory(@Param("category") String category, 
                                          @Param("subCategory") String subCategory, 
                                          @Param("collection") String collection);
    String findThumbnailRefByArchive(@Param("archiveRef") String archiveRef);
    byte[] findAssetContent(@Param("archiveRef") String archiveRef, @Param("fileName") String fileName);
}
</mediaasset>

<?xml version="1.0" encoding="UTF-8" ?>

<mapper namespace="fr.tech.gallery.repository.CatalogRepository">

    <insert id="insertCatalogItem" parameterType="fr.tech.gallery.model.CatalogItem">
        INSERT INTO t_catalog (category, sub_category, collection, thumbnail_ref, archive_ref)
        VALUES (#{category}, #{subCategory}, #{collection}, #{thumbnailRef}, #{archiveRef})
    </insert>

    <insert id="saveMediaAsset" parameterType="fr.tech.gallery.model.MediaAsset">
        INSERT INTO t_media_assets (archive_ref, is_thumbnail, file_name, content)
        VALUES (#{archiveRef}, #{isThumbnail}, #{fileName}, #{content})
    </insert>

    <select id="findAssetsByCategory" resultType="fr.tech.gallery.model.MediaAsset">
        SELECT m.archive_ref, m.is_thumbnail, m.file_name 
        FROM t_media_assets m
        INNER JOIN t_catalog c ON m.archive_ref = c.archive_ref
        WHERE c.category = #{category} AND c.sub_category = #{subCategory} AND c.collection = #{collection}
    </select>

    <select id="findThumbnailRefByArchive" resultType="string">
        SELECT thumbnail_ref FROM t_catalog WHERE archive_ref = #{archiveRef}
    </select>

    <select id="findAssetContent" resultType="byte[]">
        SELECT content FROM t_media_assets WHERE archive_ref = #{archiveRef} AND file_name = #{fileName}
    </select>

</mapper>

Couche Service et Contrôleur

Le service encapsule la logique métier, tandis que le contrôleur gère les requêtes HTTP multipart. L'implémentation suivante corrige les vulnérabilités de lecture de flux de l'ancienne version en utilisant readAllBytes() et gère les archives ZIP et RAR avec des blocs try-with-resources pour garantir la fermeture des descripteurs de fichiers.


package fr.tech.gallery.service;

import fr.tech.gallery.model.CatalogItem;
import fr.tech.gallery.model.MediaAsset;
import fr.tech.gallery.repository.CatalogRepository;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class CatalogService {
    private final CatalogRepository repository;

    public CatalogService(CatalogRepository repository) {
        this.repository = repository;
    }

    public boolean createCatalogItem(CatalogItem item) {
        return repository.insertCatalogItem(item) > 0;
    }

    public void storeAsset(MediaAsset asset) {
        repository.saveMediaAsset(asset);
    }

    public String getExpectedThumbnailName(String archiveRef) {
        return repository.findThumbnailRefByArchive(archiveRef);
    }

    public List<mediaasset> getGalleryItems(String category, String subCategory, String collection) {
        return repository.findAssetsByCategory(category, subCategory, collection);
    }

    public byte[] getAssetData(String archiveRef, String fileName) {
        return repository.findAssetContent(archiveRef, fileName);
    }
}
</mediaasset>

package fr.tech.gallery.controller;

import com.github.junrar.Archive;
import com.github.junrar.rarfile.FileHeader;
import fr.tech.gallery.model.CatalogItem;
import fr.tech.gallery.model.MediaAsset;
import fr.tech.gallery.service.CatalogService;
import fr.tech.gallery.util.StorageManager;
import fr.tech.gallery.util.ThumbnailGenerator;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

@RestController
@RequestMapping("/api/gallery")
public class AssetController {

    private final CatalogService catalogService;

    public AssetController(CatalogService catalogService) {
        this.catalogService = catalogService;
    }

    @PostMapping("/catalog")
    public ResponseEntity<string> addCatalogItem(@RequestBody CatalogItem item) {
        return catalogService.createCatalogItem(item) ? ResponseEntity.ok("Succès") : ResponseEntity.badRequest().body("Échec");
    }

    @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<string> processArchiveUpload(@RequestParam("archiveFile") MultipartFile file,
                                                       @RequestParam("type") String uploadType) {
        if (file.isEmpty()) return ResponseEntity.badRequest().body("Fichier vide");

        Path tempFile = null;
        try (InputStream inputStream = file.getInputStream()) {
            tempFile = StorageManager.saveTempFile(inputStream, file.getOriginalFilename());
            String archiveName = file.getOriginalFilename();

            if (archiveName.toLowerCase().endsWith(".zip")) {
                processZipArchive(tempFile.toFile(), archiveName, uploadType);
            } else if (archiveName.toLowerCase().endsWith(".rar")) {
                processRarArchive(tempFile.toFile(), archiveName, uploadType);
            } else {
                return ResponseEntity.badRequest().body("Format non supporté");
            }
            return ResponseEntity.ok("Traitement terminé");
        } catch (Exception e) {
            return ResponseEntity.internalServerError().body("Erreur lors du traitement : " + e.getMessage());
        } finally {
            StorageManager.cleanup(tempFile);
        }
    }

    private void processZipArchive(File archiveFile, String archiveName, String uploadType) throws IOException {
        try (ZipFile zipFile = new ZipFile(archiveFile, StandardCharsets.UTF_8)) {
            Enumeration extends ZipEntry> entries = zipFile.entries();
            String expectedThumb = catalogService.getExpectedThumbnailName(archiveName);

            while (entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                if (entry.isDirectory()) continue;

                try (InputStream entryStream = zipFile.getInputStream(entry)) {
                    byte[] fileContent = readAllBytes(entryStream);
                    String fileName = entry.getName();

                    catalogService.storeAsset(new MediaAsset(archiveName, false, fileName, fileContent));

                    if (expectedThumb != null && fileName.equalsIgnoreCase(expectedThumb)) {
                        byte[] thumbnailData = ThumbnailGenerator.resizeImage(new ByteArrayInputStream(fileContent), 150, 150, "jpg");
                        String thumbName = ThumbnailGenerator.generateThumbName(fileName);
                        catalogService.storeAsset(new MediaAsset(archiveName, true, thumbName, thumbnailData));
                    }
                }
            }
        }
    }

    private void processRarArchive(File archiveFile, String archiveName, String uploadType) throws Exception {
        try (Archive rarArchive = new Archive(archiveFile)) {
            FileHeader fileHeader = rarArchive.nextFileHeader();
            String expectedThumb = catalogService.getExpectedThumbnailName(archiveName);

            while (fileHeader != null) {
                if (!fileHeader.isDirectory()) {
                    try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
                        rarArchive.extractFile(fileHeader, outputStream);
                        byte[] fileContent = outputStream.toByteArray();
                        String fileName = fileHeader.getFileNameString();

                        catalogService.storeAsset(new MediaAsset(archiveName, false, fileName, fileContent));

                        if (expectedThumb != null && fileName.equalsIgnoreCase(expectedThumb)) {
                            byte[] thumbnailData = ThumbnailGenerator.resizeImage(new ByteArrayInputStream(fileContent), 150, 150, "jpg");
                            String thumbName = ThumbnailGenerator.generateThumbName(fileName);
                            catalogService.storeAsset(new MediaAsset(archiveName, true, thumbName, thumbnailData));
                        }
                    }
                }
                fileHeader = rarArchive.nextFileHeader();
            }
        }
    }

    private byte[] readAllBytes(InputStream inputStream) throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        byte[] data = new byte[8192];
        int bytesRead;
        while ((bytesRead = inputStream.read(data, 0, data.length)) != -1) {
            buffer.write(data, 0, bytesRead);
        }
        return buffer.toByteArray();
    }

    @GetMapping("/image")
    public void streamImage(@RequestParam String archiveRef, @RequestParam String fileName, HttpServletResponse response) throws IOException {
        byte[] imageData = catalogService.getAssetData(archiveRef, fileName);
        if (imageData != null) {
            response.setContentType("image/jpeg");
            response.setContentLength(imageData.length);
            try (OutputStream out = response.getOutputStream()) {
                out.write(imageData);
            }
        } else {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
    }
}
</string></string>

Interface Utilisateur (JSP)

Les templates JSP suivants fournissent un point d'entrée pour le téléversement des archives et la visualisation de la galerie d'images. La structure a été épurée pour se concentrer sur la sémantique HTML5.


<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Téléversement d'Archives Multimédias</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 40px; }
        .form-group { margin-bottom: 15px; }
        label { display: block; margin-bottom: 5px; font-weight: bold; }
        button { padding: 10px 15px; background-color: #007bff; color: white; border: none; cursor: pointer; }
    </style>
</head>
<body>
    <h1>Importer des Collections d'Images</h1>
    <form action="${pageContext.request.contextPath}/api/gallery/upload" method="post" enctype="multipart/form-data">
        <div class="form-group">
            <label for="archiveType">Type d'Archive :</label>
            <select name="type" id="archiveType">
                <option value="single">Archive Unique</option>
                <option value="batch">Traitement par Lots</option>
            </select>
        </div>
        <div class="form-group">
            <label for="fileInput">Sélectionner un fichier (.zip ou .rar) :</label>
            <input type="file" id="fileInput" name="archiveFile" required>
        </div>
        <button type="submit">Lancer l'Importation</button>
    </form>
</body>
</html>


<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Galerie de la Collection</title>
    <style>
        .gallery { display: flex; flex-wrap: wrap; gap: 20px; margin-top: 20px; }
        .asset-card { border: 1px solid #ddd; padding: 10px; text-align: center; width: 200px; }
        .asset-card img { max-width: 100%; height: auto; }
    </style>
</head>
<body>
    <h1>Images de la Collection : ${param.collection}</h1>
    <div class="gallery">
        <c:forEach var="asset" items="${assets}">
            <div class="asset-card">
                <img src="${pageContext.request.contextPath}/api/gallery/image?archiveRef=${asset.archiveRef}&fileName=${asset.fileName}" alt="${asset.fileName}">
                <p>${asset.fileName}</p>
            </div>
        </c:forEach>
    </div>
</body>
</html>

Étiquettes: Java Spring MVC MyBatis ImageIO ZipArchive

Publié le 13 juillet à 23h42