illustration de l'article sur Quarkus SFTP

SFTP + PostgreSQL : Construire un système de téléchargement de fichiers avec Quarkus Dev Services

En prenant mon café ce matin, je suis tombé sur un excellent article technique de The Main Thread qui présentait une approche vraiment élégante pour gérer les fichiers en entreprise avec Quarkus. L’auteur montrait comment dépasser les simples tutoriels « hello world » pour construire un vrai système de gestion de fichiers qui pourrait fonctionner en production. J’ai donc décidé de traduire cet article et d’y ajouter mes propres explications pour vous aider à comprendre les concepts en profondeur.

Ce qui m’a particulièrement séduit dans cette approche, c’est la façon dont elle combine la simplicité de développement avec la robustesse nécessaire en entreprise. Nous allons voir ensemble comment construire un système qui non seulement transfère des fichiers de manière sécurisée, mais qui garde également une trace complète de tous les échanges pour l’audit et la traçabilité.

Note : Si vous débutez avec les concepts de gestion mémoire en Java, je vous recommande de consulter d’abord notre guide sur les fuites mémoire en Java pour bien comprendre l’importance de la gestion des ressources, concept central dans ce tutoriel.

Pourquoi ce tutoriel est important ?

La plupart des tutoriels sur la gestion de fichiers s’arrêtent aux endpoints REST et au système de fichiers local. C’est suffisant pour des démos « hello world », mais pas pour de vrais systèmes.

Dans l’entreprise, les transferts de fichiers se font généralement via des protocoles sécurisés comme SFTP, et les métadonnées de ces fichiers (qui a envoyé quoi, quand, et quelle taille) doivent être suivies de manière fiable pour l’audit, l’intégration ou le traitement en aval. Pensez à des cas d’usage comme l’ingestion de factures, les déclencheurs de jobs batch, la conformité réglementaire, ou l’échange sécurisé de documents.

Dans ce guide pratique, vous allez construire un système de gestion de fichiers basé sur Quarkus qui reflète ces exigences du monde réel. Les fichiers seront téléchargés de manière sécurisée vers un serveur SFTP, et chaque téléchargement stockera des métadonnées structurées (comme le nom de fichier, la taille et l’horodatage) dans une base de données PostgreSQL utilisant Hibernate Panache. Tout cela fonctionne en mode développement sans configuration manuelle de conteneurs, grâce aux Quarkus Dev Services et Compose Dev Services.

Vous obtenez la vitesse de Quarkus, la structure d’un modèle de données approprié, et un environnement de développement reproductible — tous les ingrédients essentiels pour des workflows de fichiers robustes et prêts pour la production.

Ce que vous allez construire

À la fin de ce tutoriel, vous aurez un système fonctionnel avec :

  • Un serveur SFTP lancé via Docker Compose
  • Une base de données PostgreSQL gérée par Quarkus Dev Services
  • Une entité FileMetadata stockée avec Hibernate Panache
  • Une API REST pour télécharger des fichiers, les télécharger, et interroger les métadonnées

Prérequis

Assurez-vous d’avoir les bases installées :

  • Java 17+
  • Maven 3.8.1+
  • Podman + (Podman Compose ou Docker Compose)
  • Votre IDE favori (VS Code ou IntelliJ IDEA)

Podman est un logiciel libre permettant de lancer des applications dans des conteneurs logiciels.

Créer le projet

Commencez par générer une nouvelle application Quarkus avec toutes les bonnes extensions :

quarkus create app com.example:quarkus-sftp-compose \
--extension='rest-jackson,hibernate-orm-panache,jdbc-postgresql' \
--no-code
cd quarkus-sftp-compose

Cela vous donne une base propre avec REST, Panache ORM, PostgreSQL JDBC, et le support d’images Docker.

Ajouter la dépendance JSch

Nous utiliserons le fork mwiede/jsch pour gérer SFTP :

<dependency>
    <groupId>com.github.mwiede</groupId>
    <artifactId>jsch</artifactId>
    <version>2.27.2</version>
</dependency>

Définir compose-devservices.yml

Créez un fichier compose-devservices.yml à la racine du projet. Il définira un serveur SFTP :

services:
  sftp:
    image: atmoz/sftp:latest
    container_name: my-sftp-dev
    volumes:
      - ./target/sftp_data:/home/testuser/upload
    ports:
      - "2222:22"
    command: testuser:testpass:::upload
    labels:
      io.quarkus.devservices.compose.wait_for.logs: .*Server listening on :: port 22.*

Quarkus détectera et lancera ceci pour vous pendant quarkus dev.

Configurer application.properties

Maintenant, connectez tout en éditant src/main/resources/application.properties :

# Configuration SFTP
sftp.host=localhost
sftp.port=2222
sftp.user=testuser
sftp.password=testpass
sftp.remote.directory=/upload

# PostgreSQL
quarkus.datasource.db-kind=postgresql

# Hibernate ORM
quarkus.hibernate-orm.database.generation=drop-and-create

Cette configuration assure que Quarkus lancera automatiquement les conteneurs PostgreSQL et SFTP avant de démarrer votre app.

Créer l’entité FileMetadata

Créons une entité simple pour suivre les fichiers téléchargés. Créez src/main/java/com/example/FileMetadata.java :

package com.example;

import java.time.LocalDateTime;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;

@Entity
public class FileMetadata extends PanacheEntity {
    @Column(unique = true, nullable = false)
    public String fileName;
    
    public long fileSize;
    
    public LocalDateTime uploadTimestamp;
}

Grâce à Panache, vous obtenez un champ id par défaut et des méthodes d’accès CRUD faciles prêtes à l’emploi.

Construire le service SFTP

Maintenant créez src/main/java/com/example/SftpService.java. Ce service va :

  • Télécharger des fichiers vers le conteneur SFTP
  • Sauvegarder les métadonnées dans la base de données
  • Gérer le téléchargement de fichiers
package com.example;

import java.io.InputStream;
import java.time.LocalDateTime;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;

@ApplicationScoped
public class SftpService {
    @ConfigProperty(name = "sftp.host")
    String host;
    
    @ConfigProperty(name = "sftp.port")
    int port;
    
    @ConfigProperty(name = "sftp.user")
    String user;
    
    @ConfigProperty(name = "sftp.password")
    String password;
    
    @ConfigProperty(name = "sftp.remote.directory")
    String remoteDirectory;

    @Transactional // Crucial pour les opérations de base de données
    public FileMetadata uploadFile(String fileName, long fileSize, InputStream inputStream)
            throws JSchException, SftpException {
        // 1. Télécharger le fichier vers SFTP
        ChannelSftp channelSftp = createSftpChannel();
        try {
            channelSftp.connect();
            String remoteFilePath = remoteDirectory + "/" + fileName;
            channelSftp.put(inputStream, remoteFilePath);
        } finally {
            disconnectChannel(channelSftp);
        }

        // 2. Persister les métadonnées dans la base de données
        FileMetadata meta = new FileMetadata();
        meta.fileName = fileName;
        meta.fileSize = fileSize;
        meta.uploadTimestamp = LocalDateTime.now();
        meta.persist(); // Panache rend la sauvegarde simple !
        
        return meta;
    }

    public InputStream downloadFile(String fileName) throws JSchException, SftpException {
        ChannelSftp channelSftp = createSftpChannel();
        channelSftp.connect();
        String remoteFilePath = remoteDirectory + "/" + fileName;
        return new SftpInputStream(channelSftp.get(remoteFilePath), channelSftp);
    }

    // Méthodes d'aide privées
    private ChannelSftp createSftpChannel() throws JSchException {
        JSch jsch = new JSch();
        Session session = jsch.getSession(user, host, port);
        session.setPassword(password);
        java.util.Properties config = new java.util.Properties();
        config.put("StrictHostKeyChecking", "no");
        session.setConfig(config);
        session.connect();
        return (ChannelSftp) session.openChannel("sftp");
    }

    private void disconnectChannel(ChannelSftp channel) {
        if (channel != null) {
            if (channel.isConnected()) {
                channel.disconnect();
            }
            try {
                if (channel.getSession() != null && channel.getSession().isConnected()) {
                    channel.getSession().disconnect();
                }
            } catch (JSchException e) {
                // Ignorer
            }
        }
    }

    private class SftpInputStream extends InputStream {
        private final InputStream sftpStream;
        private final ChannelSftp channelToDisconnect;

        public SftpInputStream(InputStream sftpStream, ChannelSftp channel) {
            this.sftpStream = sftpStream;
            this.channelToDisconnect = channel;
        }

        @Override
        public int read() throws java.io.IOException {
            return sftpStream.read();
        }

        @Override
        public void close() throws java.io.IOException {
            sftpStream.close();
            disconnectChannel(channelToDisconnect);
        }
    }
}

Construire l’API REST

Créez src/main/java/com/example/SftpResource.java. Cela expose nos trois endpoints :

package com.example;

import java.io.InputStream;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
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.MediaType;
import jakarta.ws.rs.core.Response;

@Path("/files")
public class SftpResource {
    @Inject
    SftpService sftpService;

    @POST
    @Path("/upload/{fileName}")
    @Consumes(MediaType.APPLICATION_OCTET_STREAM)
    @Produces(MediaType.APPLICATION_JSON)
    public Response uploadFile(@PathParam("fileName") String fileName, 
                              @HeaderParam("Content-Length") long fileSize,
                              InputStream fileInputStream) {
        try {
            FileMetadata meta = sftpService.uploadFile(fileName, fileSize, fileInputStream);
            return Response.status(Response.Status.CREATED).entity(meta).build();
        } catch (Exception e) {
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity("Failed to upload file: " + e.getMessage()).build();
        }
    }

    @GET
    @Path("/meta/{fileName}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response getMetadata(@PathParam("fileName") String fileName) {
        return FileMetadata.find("fileName", fileName)
                .firstResultOptional()
                .map(meta -> Response.ok(meta).build())
                .orElse(Response.status(Response.Status.NOT_FOUND).build());
    }

    @GET
    @Path("/download/{fileName}")
    @Produces(MediaType.APPLICATION_OCTET_STREAM)
    public Response downloadFile(@PathParam("fileName") String fileName) {
        try {
            InputStream inputStream = sftpService.downloadFile(fileName);
            return Response.ok(inputStream)
                    .header("Content-Disposition", "attachment; filename=\"" + fileName + "\"")
                    .build();
        } catch (Exception e) {
            if (e instanceof com.jcraft.jsch.SftpException
                    && ((com.jcraft.jsch.SftpException) e).id == com.jcraft.jsch.ChannelSftp.SSH_FX_NO_SUCH_FILE) {
                return Response.status(Response.Status.NOT_FOUND)
                        .entity("File not found: " + fileName).build();
            }
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
                    .entity("Failed to download file: " + e.getMessage()).build();
        }
    }
}

Lancer et tester tout

Démarrez l’app :

quarkus dev

Regardez-la télécharger et démarrer automatiquement vos services SFTP et PostgreSQL.

Télécharger un fichier

echo "Hello from Dev Services!" > my-file.txt
curl -X POST -H "Content-Type: application/octet-stream" \
  --data-binary "@my-file.txt" \
  http://localhost:8080/files/upload/my-file.txt

Vérifier les métadonnées

curl http://localhost:8080/files/meta/my-file.txt

Vous devriez voir une réponse JSON avec les métadonnées du fichier.

Télécharger le fichier

curl http://localhost:8080/files/download/my-file.txt -o downloaded.txt
cat downloaded.txt

Vous pouvez aussi vérifier dans le conteneur si le fichier existe vraiment dans le serveur SFTP :

podman exec my-sftp-dev ls /home/testuser/upload

Ce que vous avez appris

Vous avez maintenant combiné :

  • Les transferts de fichiers SFTP
  • La persistance PostgreSQL via Panache
  • Quarkus Dev Services avec Compose Dev Services

Tout cela d’une manière qui imite un vrai scenario de production, tout en restant convivial pour les développeurs.

Pour aller plus loin

Vous pourriez :

  • Ajouter l’authentification pour le téléchargement/téléversement
  • Suivre l’historique des versions par fichier
  • Intégrer avec OpenTelemetry pour l’observabilité
  • Construire une UI utilisant Qute ou intégrer avec React

Mais pour l’instant, vous avez un service de gestion de fichiers full-stack et intelligent construit en moins d’une heure.

Référentiel de code source : https://github.com/myfear/ejq_substack_articles/tree/main/quarkus-sftp-compose


Notes techniques pour approfondir votre compréhension

Concepts fondamentaux expliqués

Commençons par démystifier les technologies utilisées dans ce tutoriel. Quarkus est un framework Java relativement récent qui a été conçu spécifiquement pour l’ère du cloud. Contrairement aux frameworks Java traditionnels comme Spring Boot qui peuvent prendre plusieurs secondes à démarrer et consommer des centaines de mégaoctets de mémoire, Quarkus démarre en quelques millisecondes et utilise une fraction de la mémoire. Cette performance est obtenue grâce à des techniques comme la compilation native et l’optimisation au moment de la construction.

SFTP (SSH File Transfer Protocol) représente l’évolution sécurisée du simple FTP. Imaginez que vous voulez envoyer un document confidentiel : vous ne l’enverriez pas par carte postale, mais dans une enveloppe scellée avec accusé de réception. SFTP fait exactement cela pour vos fichiers numériques, en chiffrant les données pendant le transport et en fournissant une authentification robuste.

Hibernate Panache mérite une attention particulière car il révolutionne la façon dont nous travaillons avec les bases de données en Java. Traditionnellement, pour sauvegarder une entité, vous deviez créer un repository, injecter un EntityManager, gérer les transactions manuellement, et écrire beaucoup de code répétitif. Avec Panache, votre entité hérite de PanacheEntity et vous pouvez simplement appeler entity.persist(). C’est comme passer d’une voiture manuelle à une automatique : le résultat est le même, mais l’expérience est infiniment plus fluide.

Pour approfondir la compréhension de JPA et Hibernate, consultez notre guide détaillé sur le cache de premier niveau JPA et Hibernate qui explique les mécanismes internes de persistance.

Pourquoi cette architecture est intelligente

L’architecture proposée dans ce tutoriel illustre parfaitement le principe de séparation des préoccupations, un concept fondamental en ingénierie logicielle. Pensez à votre bureau : vous rangez vos documents papier dans des classeurs (stockage physique), mais vous tenez un registre dans votre agenda de ce que vous avez archivé et quand (métadonnées). Cette séparation permet d’optimiser chaque aspect selon ses besoins spécifiques.

Dans notre système, les fichiers physiques sont stockés sur le serveur SFTP, qui est optimisé pour la sécurité et la fiabilité du stockage. Les métadonnées vivent dans PostgreSQL, une base de données relationnelle qui excelle dans les requêtes complexes et les jointures. Cette séparation nous permet de répondre rapidement à des questions comme « quels fichiers ont été uploadés la semaine dernière par l’utilisateur X ? » sans avoir à parcourir physiquement tous les fichiers.

L’API REST agit comme un chef d’orchestre, coordonnant les interactions entre ces deux systèmes. Elle s’assure que chaque opération respecte le principe de cohérence : soit tout réussit (fichier sauvegardé ET métadonnées enregistrées), soit tout échoue. Cette approche « tout ou rien » est essentielle en environnement de production où la cohérence des données est cruciale.

Si vous travaillez régulièrement avec de gros volumes de données, notre article sur les opérations en masse avec JPA et Hibernate vous montrera comment optimiser les performances quand vous devez traiter de nombreux fichiers simultanément.

La magie des Dev Services

Les Dev Services de Quarkus représentent une innovation majeure dans l’expérience développeur. Imaginez que vous emménagiez dans un nouvel appartement et que tous les meubles, l’électricité et l’eau soient automatiquement installés dès que vous ouvrez la porte. C’est exactement ce que font les Dev Services pour votre environnement de développement.

Quand vous lancez quarkus dev, le framework analyse vos dépendances et vos fichiers de configuration. Il détecte que vous utilisez PostgreSQL et trouve votre fichier compose-devservices.yml. Automatiquement, il télécharge les images Docker nécessaires, lance les conteneurs avec la bonne configuration réseau, attend que les services soient prêts, et configure votre application pour s’y connecter. Tout cela se passe en arrière-plan pendant que vous pouvez vous concentrer sur votre code métier.

Cette approche élimine l’un des plus gros obstacles au développement moderne : la configuration de l’environnement. Plus besoin de documents WikiSSL LS de 50 pages expliquant comment installer et configurer PostgreSQL, plus de « ça marche sur ma machine » entre développeurs. Chaque développeur de l’équipe obtient exactement le même environnement, reproductible et fonctionnel.

Points techniques importants

L’annotation @Transactional sur la méthode uploadFile() mérite qu’on s’y attarde car elle illustre un concept fondamental des systèmes distribués : la cohérence transactionnelle. Dans notre cas, nous effectuons deux opérations distinctes : sauvegarder un fichier sur SFTP et enregistrer ses métadonnées en base. Ces opérations peuvent échouer indépendamment l’une de l’autre. L’annotation @Transactional garantit que si l’une échoue, l’autre sera également annulée, préservant ainsi la cohérence de notre système.

La classe SftpInputStream représente un exemple élégant du pattern « Resource Management » en Java. Le problème avec les connexions réseau est qu’elles consomment des ressources système limitées qui doivent être libérées proprement. Cette classe encapsule le flux de données SFTP et s’assure que la connexion se ferme automatiquement quand le fichier est entièrement lu, même en cas d’erreur. C’est une implémentation du pattern « try-with-resources » adapté aux spécificités de SFTP.

La configuration via @ConfigProperty illustre l’approche « 12-factor app » pour la gestion de la configuration. Plutôt que de coder en dur les paramètres de connexion, ils sont externalisés dans des fichiers de propriétés ou des variables d’environnement. Cela permet de déployer la même application dans différents environnements (développement, test, production) avec des configurations différentes, sans modification du code.

Cette approche architecturale vous donne les fondations solides pour construire des systèmes de gestion de fichiers robustes et évolutifs, tout en maintenant une excellente expérience développeur grâce aux innovations de Quarkus.

Pour rester informé des dernières évolutions de Java qui pourraient enrichir ce type d’architecture, consultez notre section JEPs qui explique les nouveautés du langage comme l’API Gatherer de Java 25 ou les imports de modules.

Nos derniers articles

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

📚 Nouveau sur Java ? Mon livre vous guide pas à pas dans l'apprentissage !

X