30-4 Contrôleur d'API pour recevoir un fichier
Pour recevoir des fichiers et les stocker, nous allons créer un nouveau contrôleur dont le rôle sera précisément cela: recevoir un fichier et le stocker dans le dossier Storage
.
Créer le contrôleur UploadsController
- Sous le projet
Snowfall.Web.Api
, sous le dossierControllers
->Add
->Scaffolded Item
- Choisir
API Controller - Empty
- Nommer le contrôleur
UploadsController
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Snowfall.Web.Api.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class UploadsController : ControllerBase
{
}
}
Injecter IWebHostEnvironment
et IConfiguration
Pour avoir accès au dossier courant, nous aurons besoin de IWebHostEnvironement
et pour accéder à appsettings.json
qui contient les détails sur le dossier de stockage il nous faudra IConfiguration
.
Injectons ces derniers via le constructeur du nouveau contrôleur:
public class UploadsController : ControllerBase
{
private readonly IWebHostEnvironment _environment;
private readonly IConfiguration _configuration;
public UploadsController(IWebHostEnvironment environment,
IConfiguration configuration)
{
_environment = environment;
_configuration = configuration;
}
}
Créer l'action Create
via POST
Les fichiers seront reçus via une requête HTTP
POST
, avec en argument un fichier.
Créons la coquille de cette fonction:
[HttpPost]
public async Task<IActionResult> Create([FromForm] IFormFile file)
{
}
[FromForm]
est un attribut utilisé pour indiquer que la valeur du paramètre doit être obtenue à partir des données de formulaire envoyées par le client lors de la requête HTTP. Cela est couramment utilisé lors de la soumission de formulaires avec des données de type multipart/form-data
pour les téléversements de fichiers, par exemple.
Lorsque vous utilisez [FromForm]
dans un contrôleur API, il indique au cadriciel de rechercher la valeur du paramètre dans les données du formulaire plutôt que dans d'autres sources potentielles comme l'URI de la requête, les données JSON du corps de la requête ou les en-têtes HTTP.
Créer un DTO ResultatUpload
Le contrôleur retournera de l'information sur le fichier. Pour contenir cette information, on utilisera un DTO.
Il sera nécessaire de retourner au client les informations suivantes:
- Si le téléversement a été un succès
- Nom du fichier
- URL du fichier
- Erreur, si pertinent.
- Sous le projet
Snowfall.Application
, sous le dossierDtos
->Add
->Directory
. - Nommez le dossier
Uploads
- Sous le nouveau dossier
Uploads
->Add
->Class/Interface
- Nommez la classe
ResultatUpload
public enum ErreurUpload
{
TailleMax,
FichierVide,
Ecriture,
Lecture
}
public class ResultatUpload
{
public bool EstSucces { get; set; }
public string? NomFichier { get; set; }
public string? UrlFichier { get; set; }
public ErreurUpload? Erreur { get; set; }
}
Pour représenter les différents codes d'erreur, on peut utiliser un enum
. Cela est flexible et descriptif et permet de gérer facilement plusieurs cas de figure.
L'utilisation d'un enum
a du sens dans le cadre d'un projet où les même DTO sont partagés par le client et le serveur. En effet, au besoin, le client a accès à l'enum de son côté et saura qu'une valeur de "0" correspond à TailleMax
.
Créer la base de la fonction Create
On commence par créer quelques variables de base dont nous aurons besoin pour traiter le fichier reçu.
[HttpPost]
public async Task<IActionResult> Create([FromForm] IFormFile file)
{
// Nom de fichier source tel que reçu du client
string nomFichierSource = file.FileName;
// La taille maximale de fichier acceptée (5 Mo)
long tailleFichierMax = 1024 * 1024 * 5; // 5mb
// L'URL de base qui sera utilisée pour retourner l'URL du fichier
var baseResourceUrl = new Uri($"{Request.Scheme}://{Request.Host}");
}
De plus, on peut initialiser le DTO de résultat de téléversement avec la variable EstSucces
à false
. true
sera assigné à cette variable lorsque le téléversement sera complété, à condition qu'aucune erreur n'ait été rencontrée.
public async Task<IActionResult> Create([FromForm] IFormFile file)
{
// Nom de fichier source tel que reçu du client
string nomFichierSource = file.FileName;
// La taille maximale de fichier acceptée (5 Mo)
long tailleFichierMax = 1024 * 1024 * 5; // 5mb
// L'URL de base qui sera utilisée pour retourner l'URL du fichier
var baseResourceUrl = new Uri($"{Request.Scheme}://{Request.Host}");
var resultatUpload = new ResultatUpload()
{
EstSucces = false,
};
}
Ensuite, une vérification est effectuée afin de s'assurer que la taille du fichier correspond aux attentes. Si ce n'est pas le cas, on retourne BadRequest
.
public async Task<IActionResult> Create([FromForm] IFormFile file)
{
// Nom de fichier source tel que reçu du client
string nomFichierSource = file.FileName;
// La taille maximale de fichier acceptée (5 Mo)
long tailleFichierMax = 1024 * 1024 * 5; // 5mb
// L'URL de base qui sera utilisée pour retourner l'URL du fichier
var baseResourceUrl = new Uri($"{Request.Scheme}://{Request.Host}");
var resultatUpload = new ResultatUpload()
{
EstSucces = false,
};
if (file.Length == 0)
resultatUpload.Erreur = ErreurUpload.FichierVide;
else if (file.Length > tailleFichierMax)
resultatUpload.Erreur = ErreurUpload.TailleMax;
if(resultatUpload.Erreur is not null)
return BadRequest(resultatUpload);
}
Procéder à la sauvegarde du fichier
On peut finalement sauvegarder le fichier sur le disque!
Pour écrire un fichier sur le disque, la classe FileStream
peut être utilisée.
Vous remarquerez que les fichiers reçus sont renommés à l'aide d'un GUID
.
Les GUID (Globally Unique Identifiers) sont des identifiants uniques, générés aléatoirement, qui peuvent être utilisés pour nommer les fichiers stockés. Ils présentent plusieurs avantages par rapport aux noms de fichiers traditionnels basés sur des noms d'utilisateur, des dates ou des séquences numériques.
Le plus important, dans notre cas, est que les GUID garantissent l'unicité des noms de fichiers, ce qui évite les conflits et les écrasements involontaires de fichiers existants. De plus ils rendent les noms de fichiers impossibles à deviner, ce qui dans certain cas peut assurer une sorte de sécurité à ces derniers (si pertinent).
public async Task<IActionResult> Create([FromForm] IFormFile file)
{
// Nom de fichier source tel que reçu du client
string nomFichierSource = file.FileName;
// La taille maximale de fichier acceptée (5 Mo)
long tailleFichierMax = 1024 * 1024 * 5; // 5mb
// L'URL de base qui sera utilisée pour retourner l'URL du fichier
var baseResourceUrl = new Uri($"{Request.Scheme}://{Request.Host}");
var resultatUpload = new ResultatUpload()
{
EstSucces = false,
};
if (file.Length == 0)
resultatUpload.Erreur = ErreurUpload.FichierVide;
else if (file.Length > tailleFichierMax)
resultatUpload.Erreur = ErreurUpload.TailleMax;
if(resultatUpload.Erreur is not null)
return BadRequest(resultatUpload);
try
{
// On récupère l'extension du fichier
string extension = Path.GetExtension(nomFichierSource);
// On crée un nom de fichier unique à l'aide d'un GUID pour éviter
// les collisions si on soumettait plusieurs fois le même nom de fichier
string nomFichier = Guid.NewGuid().ToString();
// Le nom de fichier final complet est créé
string nomFichierStorage = nomFichier + extension;
// Le chemin de sauvegarde est le dossier Storage
string path = Path.Combine(_environment.ContentRootPath,
_configuration["DossierStorage"]!,
nomFichierStorage);
// Écriture du fichier sur le disque
await using FileStream fs = new(path, FileMode.Create);
await file.CopyToAsync(fs);
// Compléter le DTO
resultatUpload.EstSucces = true;
resultatUpload.NomFichier = nomFichierStorage;
resultatUpload.UrlFichier = $"{baseResourceUrl}storage/{nomFichierStorage}";
}
catch (IOException ex)
{
resultatUpload.Erreur = ErreurUpload.Ecriture;
return BadRequest(resultatUpload);
}
// Retour 201 Created avec le DTO
return Created(resultatUpload.UrlFichier!, resultatUpload);
}
Test du contrôleur
Le contrôleur peut être testé à l'aide de Postman. En effet, il est possible dans Postman de choisir pour un paramètre (key) de formulaire (form-data
), un type:
- Text
- File
Si vous faites la sélection pour File
comme type, il est ensuite possible de sélectionner un fichier et de l'envoyer avec la requête.
Si vous faites Send
et que tout se passe comme prévu, vous devriez voir qu'un nouveau fichier a été ajouté au dossier Storage
😎