Déploiement et intégration de MinIO avec Spring Boot

Introduction au stockage objet

Le stockage objet est une architecture conçue pour gérer de grands volumes de données non strucutrées. Contrairement aux systèmes de fichiers traditionnels ou au stockage par blocs, il découpe les données en unités autonomes (objets) contenant les données, leurs métadonnées et un identifiant unique.

Voici une comparaison entre le stockage objet, le disque local et les systèmes de fichiers distribués :

Caractéristique Stockage objet Disque serveur Système de fichiers distribué
Unité de base Objet (données + métadonnées + ID) Fichier sur disque local Données réparties sur plusieurs nœuds
Avantages Extensibilité massive (jusqu'à l'Exaoctet), performance via structure plate, sécurité via clés d'authentification HTTP, accès REST, coût réduit Simplicité de développement, faible coût si infrastructure existante Extension horizontale facile par ajout de nœuds
Inconvénients Cohérence éventuelle, inadapté aux données très volatiles Difficulté d'extension capacitaire, remplacement coûteux Complexité de déploiement et maintenance

Un système de fichiers distribué (DFS) permet de stocker et d'accéder à des fichiers répartis sur plusieurs serveurs, offrant transparence, haute disponibilité, extensibilité et tolérance aux pannes.

Présentation de MinIO

MinIO est un système de stockage objet distribué, hautes performances, écrit en Go et compatible avec l'API Amazon S3. Il fonctionne sur du matériel standard (x86, etc.) et a été conçu dès l'origine pour les besoins exigeants du cloud privé. Il excelle dans les cas d'usage traditionnels (sauvegarde, archivage, reprise après sinistre) ainsi que dans le big data, le machine learning et les environnements hybrides. La communication entre le client et le serveur se fait via HTTP/HTTPS.

Ressources utiles :

Installation de MinIO sur CentOS 7

Téléchargement du binaire

mkdir -p /opt/minio
cd /opt/minio
wget https://dl.min.io/server/minio/release/linux-amd64/minio
chmod +x minio

Démarrage du serveur

# Création des répertoires de données et logs
mkdir -p /data/minio
mkdir -p /data/minio/log

# Démarrage en avant-plan
./minio server /data/minio

# Démarrage en arrière-plan avec ports personnalisés
nohup ./minio server --address :9000 --console-address :9001 /data/minio > /data/minio/log/minio.log 2>&1 &

Si le pare-feu est actif, ouvrir les ports 9000 et 9001 :

systemctl stop firewalld   # ou configurer les règles appropriées

Accès à l'interface d'administration : http://votre_ip:9001 (identifiants par défaut : minioadmin / minioadmin).

Configuration des identifiants

Éditer le fichier /etc/profile et ajouter :

export MINIO_ROOT_USER=monadmin
export MINIO_ROOT_PASSWORD=monmotdepasse

Puis exécuter source /etc/profile.

Script de démarrage automatique avec systemd (recommandé)

cat > /etc/systemd/system/minio.service << 'EOF'
[Unit]
Description=MinIO Server
After=network.target

[Service]
ExecStart=/opt/minio/minio server /data/minio --console-address :9001
Restart=always
Environment="MINIO_ROOT_USER=monadmin"
Environment="MINIO_ROOT_PASSWORD=monmotdepasse"

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable minio
systemctl start minio
systemctl status minio

Configuration du bucket

Dans l'interface console, régler le bucket en mode public pour permettre l'accès direct aux fichiers via URL.

Intégration avec Spring Boot

Dépendance Maven

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.5.17</version>
</dependency>

Configuration application.yml

minio:
  endpoint: http://192.168.1.100
  port: 9000
  accessKey: monadmin
  secretKey: monmotdepasse
  bucketName: documents
  secure: false

spring:
  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 150MB

Classe de configuration

import io.minio.MinioClient;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "minio")
@Getter @Setter
public class MinioProperties {
    private String endpoint;
    private int port;
    private String accessKey;
    private String secretKey;
    private Boolean secure;
    private String bucketName;

    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(endpoint, port, secure)
                .credentials(accessKey, secretKey)
                .build();
    }
}

Service utilitaire

Voici une version simplifiée du service (les méthodes d'upload, download, suppression, etc.) :

import io.minio.*;
import io.minio.errors.*;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.*;

@Service
public class MinioService {

    private final MinioClient client;
    private final MinioProperties props;

    public MinioService(MinioClient client, MinioProperties props) {
        this.client = client;
        this.props = props;
    }

    public void uploadFile(MultipartFile file, String objectName) throws Exception {
        client.putObject(
                PutObjectArgs.builder()
                        .bucket(props.getBucketName())
                        .object(objectName)
                        .stream(file.getInputStream(), file.getSize(), -1)
                        .contentType(file.getContentType())
                        .build());
    }

    public InputStream downloadFile(String objectName) throws Exception {
        return client.getObject(
                GetObjectArgs.builder()
                        .bucket(props.getBucketName())
                        .object(objectName)
                        .build());
    }

    public String getFileUrl(String objectName) throws Exception {
        return client.getObjectUrl(props.getBucketName(), objectName);
    }

    public boolean bucketExists() throws Exception {
        return client.bucketExists(
                BucketExistsArgs.builder().bucket(props.getBucketName()).build());
    }

    public void createBucket() throws Exception {
        client.makeBucket(
                MakeBucketArgs.builder().bucket(props.getBucketName()).build());
    }

    public void deleteObject(String objectName) throws Exception {
        client.removeObject(
                RemoveObjectArgs.builder()
                        .bucket(props.getBucketName())
                        .object(objectName)
                        .build());
    }

    public List<String> listObjects() throws Exception {
        List<String> names = new ArrayList<>();
        Iterable<Result<Item>> results = client.listObjects(
                ListObjectsArgs.builder().bucket(props.getBucketName()).build());
        for (Result<Item> result : results) {
            names.add(result.get().objectName());
        }
        return names;
    }
}

Contrôleur REST pour l'upload

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.time.Instant;
import java.util.*;

@RestController
@RequestMapping("/files")
@Api(tags = "Gestion des fichiers")
public class FileController {

    private final MinioService minioService;
    private final MinioProperties minioProps;

    public FileController(MinioService minioService, MinioProperties minioProps) {
        this.minioService = minioService;
        this.minioProps = minioProps;
    }

    @PostMapping("/upload")
    @ApiOperation("Upload de fichiers multiples")
    public Map<String, Object> upload(@RequestParam("files") MultipartFile[] files) {
        List<String> urls = new ArrayList<>();
        for (MultipartFile file : files) {
            try {
                String originalName = file.getOriginalFilename();
                String ext = originalName.substring(originalName.lastIndexOf('.'));
                String newName = Instant.now().toEpochMilli() + "_" + UUID.randomUUID().toString().substring(0,8) + ext;
                minioService.uploadFile(file, newName);
                String url = minioService.getFileUrl(newName);
                urls.add(url);
            } catch (Exception e) {
                return Map.of("status", 500, "message", "Erreur upload", "error", e.getMessage());
            }
        }
        return Map.of("status", 200, "message", "Succès", "urls", urls);
    }

    @GetMapping("/test")
    public String test() throws Exception {
        // Test simple : upload d'un flux
        return "Test ok, bucket existe : " + minioService.bucketExists();
    }
}

Compression d'images avant upload

Pour réduire la taille des photos prises par smartphone, on peut compresser l'image avant de l'envoyer à MinIO. La librairie Thumbnailator est utilisée ici.

Dépendance

<dependency>
    <groupId>net.coobird</groupId>
    <artifactId>thumbnailator</artifactId>
    <version>0.4.20</version>
</dependency>

Méthode utilitaire de compression

import net.coobird.thumbnailator.Thumbnails;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.UUID;

public class ImageCompressor {

    private static final String[] IMAGE_EXTENSIONS = {"jpg", "jpeg", "png", "gif", "bmp", "webp"};

    public static boolean isImage(String ext) {
        for (String img : IMAGE_EXTENSIONS) {
            if (img.equalsIgnoreCase(ext)) return true;
        }
        return false;
    }

    public static MultipartFile compress(MultipartFile file, String tempDir) throws IOException {
        String originalName = file.getOriginalFilename();
        String ext = originalName.substring(originalName.lastIndexOf('.') + 1).toLowerCase();
        long size = file.getSize();

        // On compresse seulement les images de plus de 100 Ko
        if (!isImage(ext) || size < 1024 * 100) {
            return file;
        }

        // Création d'un fichier temporaire
        String tempFileName = UUID.randomUUID().toString() + "." + ext;
        File tempFile = new File(tempDir, tempFileName);
        tempFile.getParentFile().mkdirs();

        // Déterminer le facteur de qualité selon la taille
        float quality;
        if (size < 1024 * 1024) { // < 1 Mo
            quality = 0.3f;
        } else if (size < 1024 * 1024 * 2) { // 1-2 Mo
            quality = 0.2f;
        } else { // > 2 Mo
            quality = 0.1f;
        }

        // Pour les PNG, conversion en JPG avant compression (pour éviter l'augmentation de taille)
        if ("png".equals(ext)) {
            String jpgFileName = UUID.randomUUID().toString() + ".jpg";
            File jpgFile = new File(tempDir, jpgFileName);
            Thumbnails.of(file.getInputStream())
                    .scale(1f)
                    .outputFormat("jpg")
                    .toFile(jpgFile);
            // Compression du JPG
            Thumbnails.of(jpgFile)
                    .scale(1f)
                    .outputQuality(0.25f)
                    .toFile(jpgFile);
            // Lire le JPG compressé comme MultipartFile
            FileInputStream fis = new FileInputStream(jpgFile);
            MultipartFile compressed = new MockMultipartFile("file", jpgFileName, "image/jpeg", fis);
            jpgFile.deleteOnExit();
            tempFile.delete();
            return compressed;
        } else {
            // Compression directe (jpg, jpeg, etc.)
            Thumbnails.of(file.getInputStream())
                    .scale(1f)
                    .outputQuality(quality)
                    .toFile(tempFile);
            FileInputStream fis = new FileInputStream(tempFile);
            String contentType = "image/" + ext;
            MultipartFile compressed = new MockMultipartFile("file", tempFileName, contentType, fis);
            tempFile.deleteOnExit();
            return compressed;
        }
    }
}

Dans le contrôleur d'upload, on peut alors appeler ImageCompressor.compress(file, "/tmp") avant de passer le fichier à minioService.uploadFile().

Étiquettes: minio stockage objet S3 Spring Boot CentOS

Publié le 5 juin à 23h14