Gestion de fichiers
Plusieurs applications web permettront de téléverser ou télécharger des fichiers. Notez que ceci ne fait pas partie des opérations CRUD sur les données, mais bien d'un échange de données.
Dépendences requises
Bien qu'il ne soit pas nécessaire d'ajouter de dépendances si on veut simplement permettre le téléversement et téléchargement de fichiers, nous ajouterons une dépendance à Apache Tika, un analyseur de documents. Ceci nous aidera à valider le type de fichiers lors du téléversement.
<!-- Tika, pour valider les fichiers -->
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>3.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-parsers-standard-package</artifactId>
<version>3.2.3</version>
</dependency>
Structure générale
Pour permette à l'API de gérer les fichiers, nous allons créer les structures suivantes:
- Une entité pour conserver les méta données du fichier dans la base de données (avec son repository)
- Un contrôleur pour gérer les interactions avec l'utilisateur
- Un service qui s'occupe des opérations de lecture et d'écriture
- Un service qui lie le contrôleur aux opérations de lecture et d'écriture.
Configuration
En plus des structures mentionnées ci-dessus, on peut créer une propriété de configuration pour mieux gérer les propriétés des fichiers dans l'application.
- Créer un enregistrement (record) nommé
FileStorageProperties - Lui donner les propriétés
basePathetallowedMimeTypes
@ConfigurationProperties(prefix = "app.file-storage")
public record FileStorageProperties(String basePath, Set<String> allowedMimeTypes) {
public FileStorageProperties() {
this(
"votre/chemin/stockage",
Set.of(
"image/jpeg",
"image/png",
"image/webp",
"image/gif",
"text/csv"
)
);
}
}
// Dans le fichier principal de l'application
@SpringBootApplication
// Ne pas oublier d'activer la propriété de configuration
@EnableConfigurationProperties({FileStorageProperties.class})
public class DemoApiApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApiApplication.class, args);
}
}
Dans l'exemple ci dessus, je crée également un constructeur et lui donne des valeurs par défaut. Ces propriétés peuvent être configurées dans le fichier application.properties.Il suffit d'ajouter la configuration des propriétés
app.file-storage.base-path=app.file-storage.allowed-mime-types=
Pour une liste de type MIME, consulter la documentation suivante : MDN - Liste des types MIME communs
La classe de méta données
Le but de cette classe est de conserver des informations sur les fichiers téléversés, afin qu'on puisse facilement les retrouver et qu'on puisse afficher les informations de base aux utilisateurs en cas de besoin.
Voici une configuration minimale des méta données que l'on voudrait conserver.
@Entity
@Table(name = "file_metadata")
public class FileMetaData {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String nomOriginal;
String nomStocke;
String mimeType;
}
- Avec ces informations, on peut retrouver facilement un fichier avec son
id. - On ne se fie pas au nom original pour retrouver un fichier, c'est sous le contrôle de l'uitlisateur
- Le nom stocké comprendra le chemin d'accès complet. Le nom du fichier devrait également différer du nom original.
- Le mimeType nous permettra d'indiquer à l'utilisateur le type de fichier tout simplement.
- Lors de la sauvegarde d'un fichier, ces données seront extraites de celui-ci et enregistrées dans la base de données. Cela nous sert en quelque sorte d'index pour retrouver le fichier.
Le service des opérations de lecture et d'écriture
Ce service aura besoin de deux propriétés globales qu'on définit dans le constructeur
- Une instance de FileStorageProperties
- Le chemin de base de type Path
@Service
public class FileStorageService {
private final FileStorageProperties properties;
private final Path rootPath;
public FileStorageService(FileStorageProperties properties) {
this.properties = properties;
this.rootPath = Paths.get(properties.basePath());
}
}
Ce service fera simplement les opérations de lecture et d'écriture. On y mettra donc deux fonctions, une pour stocker un fichier et l'autre pour récupérer un fichier.
// Extrait de FileStorageService
public String storeFile(InputStream inputStream, String originalFilename) throws IOException {
// Création d'un dossier avec la date du jour. Ce dossier sera créé à la racine fournie dans les propriétés
LocalDate today = LocalDate.now();
Path dateDirectory = rootPath.resolve(today.getYear() + "-" + String.format("%02d", today.getMonthValue()) + " " + String.format("%02d", today.getDayOfMonth()));
Files.createDirectories(dateDirectory);
String ext = getFileExtension(originalFilename);
// Création d'un nom unique pour le fichier
String filename = UUID.randomUUID() + (ext.isEmpty() ? "" : "." + ext);
Path path = dateDirectory.resolve(filename);
// Sauvegarde du fichier
try (OutputStream outputStream = Files.newOutputStream(path, StandardOpenOption.CREATE_NEW)) {
StreamUtils.copy(inputStream, outputStream);
}
// On retourner le chemin d'accès pour pouvoir retrouver le fichier.
return rootPath.relativize(path).toString();
}
private String getFileExtension(String filename) {
int lastDot = filename.lastIndexOf(".");
return lastDot == -1 ? "" : filename.substring(lastDot + 1);
}
// Extrait de FileStorageService
public Resource getFileResource(String storedName) throws IOException {
Path path = rootPath.resolve(storedName).normalize().toAbsolutePath();
Path normalizedRootPath = rootPath.normalize().toAbsolutePath();
// Confirmation que l'utilisateur n'essaie pas de parcourir le systèmede fichiers
if(!path.startsWith(normalizedRootPath)) {
throw new SecurityException("Access denied");
}
if(!Files.exists(path)) {
throw new FileNotFoundException("File not found");
}
return new UrlResource(path.toUri());
}
Le service utilisé par le contrôleur
Ce service comprendra les méthodes qui sont appelées par la contrôleur. Il interagira également avec le service de lecture et d'écriture pour envoyer les fichiers vers le système de fichiers ou les récupérer de ce dernier pour les envoyer à l'utilisateur.
@Service
public class FileService {
private final FileStorageProperties properties;
private final FileMetaDataRepository repository;
private final FileStorageService service;
public FileService(FileStorageProperties properties, FileMetaDataRepository repository, FileStorageService service) {
this.properties = properties;
this.repository = repository;
this.service = service;
}
}
Ce service comprendra les méthodes pour traiter un fichier téléversé avant d'en faire la sauvegarde ainsi que les méthodes pour récupérer l'information des fichiers et les fichiers en tant que tel.
// Extrait de FileService
public FileMetaData uploadFile(MultipartFile file) throws IOException {
// Valider le fichier pour raison de sécurité - voir méthode ci-dessous
validateFile(file);
String storagePath;
// Tenter de sauvegarder le fichier
try(InputStream inputStream = file.getInputStream()) {
storagePath = service.storeFile(inputStream, file.getOriginalFilename());
}
// Si le fichier est sauvegardé on crée les méta données et les sauvegarde dans la base de données.
FileMetaData fileMetaData = FileMetaData
.builder()
.nomOriginal(file.getOriginalFilename())
.nomStocke(storagePath)
.mimeType(file.getContentType())
.build();
return repository.save(fileMetaData);
}
private void validateFile(MultipartFile file) throws IOException {
// Création d'une instance de Tika pour valider le type du fichier
Tika tika = new Tika();
if(file.isEmpty()) {
throw new IllegalArgumentException("File is empty");
}
String mimeType = file.getContentType();
String magicMimeType = tika.detect(file.getInputStream());
// Vérifier si on se fait mentir par l'utilisateur sur le type de fichier (grrrrr méchant!)
if(!Objects.equals(magicMimeType, mimeType)) {
throw new SecurityException("Un méchant menteur tente de téléverser un fichier avec un type modifié");
}
if (magicMimeType == null || !properties.allowedMimeTypes().contains(mimeType)) {
throw new IllegalArgumentException("Mime type invalide");
}
}
// Extrait de FileService
public Resource getFile(Long fileId) throws IOException {
FileMetaData metadata = repository.getById(fileId);
return service.getFileResource(metadata.getNomStocke());
}
public FileMetaData getFileMetadata(Long fileId) throws IOException {
return repository.findById(fileId).orElse(null);
}
Le contrôleur
Finalement le contrôleur permet à l'utilisateur d'ajouter et récupérer des fichiers
@Controller
@RequestMapping("/files")
public class FileController {
private final FileService fileService;
public FileController(FileService fileService) {
this.fileService = fileService;
}
@PostMapping
public ResponseEntity<?> uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
try {
FileMetaData metadata = fileService.uploadFile(file);
return ResponseEntity.ok().body(metadata);
} catch (IOException ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
} catch (IllegalArgumentException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
}
@GetMapping("/{fileId}")
public ResponseEntity<Resource> getFile(@PathVariable Long fileId) {
try {
FileMetaData fileMetaData = fileService.getFileMetadata(fileId);
Resource resource = fileService.getFile(fileId);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileMetaData.getNomOriginal() + "\"")
.contentType(MediaType.parseMediaType(fileMetaData.getMimeType()))
.body(resource);
} catch (IOException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
Tester avec cURL et Postman
N'oubliez pas d'ajouter le jeton d'authentification si l'action demande d'être authentifié!
Postman
Pour tester le téléversement avec Postman, il suffit d'ajouter un fichier dans le corps de la requête.
- Sous l'onglet Body, sélectionner form-data
- Nommer la clé file (pour correspondre avec les notes) et ajouter le fichier désiré.

Pour tester la récupération dans Postman, il suffit de choisir l'option Send and download lors de la transmission de la requête.

cURL
Curl nous permet également de tester le tout en ligne de commande
Pour tester le téléversement avec cURL, il suffit de spécifier qu'on ajoute un fichier avec l'option -F.
Pour teste le réléchargement avec cURL, on ajoutera les options -OJ à la requête.
-ORécupère le résultat de la requête dans un fichier-JRécupérer le nom de fichier de l'en-têteContent-Dispositionsi elle est fournie par le serveur