30-7 EXPANSION PACK: Service FileStorage
Si vous avez ajouté et modifié des événements, vous avez peut-être remarqué que les fichiers s'accumulent dans le dossier! Il est normal que certains fichiers inutilisés soient toujours présents s'ils n'ont jamais été assignés. Dans ce cas, on met en place habituellement une tâche exécutée en arrière-plan responsable de supprimer les fichiers n'ayant jamais été utilisés.
Par contre, lorsqu'on supprime un événement, on devrait supprimer le fichier associé. Idem lors d'une modification, l'ancienne image, si applicable, devrait être supprimée.
De plus, à chaque endroit où on voudrait supprimer un fichier, par exemple dans un contrôleur, il faudrait injecter IWebHostEnvironment
et IConfiguration
pour reconstruire le chemin vers le dossier central.
Le système de stockage devrait être en quelque sorte abstrait, les contrôleurs ne devraient pas avoir à se soucier de l'implémentation.
Je vous propose de créer une classe utilitaire FileStorage
responsable de sauvegarder et supprimer les fichiers.
Les concepts utilisés sont les mêmes que présenté jusqu'à maintenant, mais extraits dans une classe utilitaire. Ainsi, les explications seront limitées, je vous fournis le code, un peu comme si on installait une librairie externe pour cette tâche.
Ce service sera ajouté au projet Shared
puisqu'il relève plus de l'infrastructure et est commun à possiblement plusieurs projets.
Installer Microsoft.AspNetCore.Http.Abstractions
- Sous le projet
Shared
->Manage NuGet Packages
- Rechercher
Microsoft.AspNetCore.Http.Abstractions
- Installer
Microsoft.AspNetCore.Http.Abstractions
dans le projetShared
Service FileStorage
- Sous le projet
Shared
->Add
->Directory
- Nommer le dossier
FileStorage
Ajouter IFileStorage
Sous Shared/FileStorage
, ajoutez une Interface IFileStorage
:
using Microsoft.AspNetCore.Http;
namespace Snowfall.Shared.FileStorage;
/// <summary>
/// Service permettant de gérer la sauvegarde et la suppression de
/// fichiers dans un dossier de stockage centralisé
/// </summary>
public interface IFileStorage
{
/// <summary>
/// Sauvegarde un nouveau fichier dans le dossier centralisé
/// </summary>
/// <param name="file">Le fichier IFormFile à sauvegarder</param>
/// <returns>Le nom du fichier sauvegardé (GUID)</returns>
Task<string?> SauvegarderFichier(IFormFile file);
/// <summary>
/// Supprime un fichier dans le dossier centralisé
/// </summary>
/// <param name="nomFichier">Le nom du fichier (GUID) à supprimer</param>
/// <returns>Booléen représentant le succès ou l'échec de l'opération</returns>
bool SupprimerFichier(string nomFichier);
}
Ajouter FileStorage
Sous Shared/FileStorage
, ajoutez une Classe FileStorage
:
using Microsoft.AspNetCore.Http;
namespace Snowfall.Shared.FileStorage
public class FileStorage : IFileStorage
{
private string _dossierStorage;
public FileStorage(string dossierStorage)
{
_dossierStorage = dossierStorage;
}
public async Task<string?> SauvegarderFichier(IFormFile file)
{
string nomFichierSource = file.FileName;
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(_dossierStorage, nomFichierStorage);
// Écriture du fichier sur le disque
await using FileStream fs = new(path, FileMode.Create);
await file.CopyToAsync(fs);
return nomFichierStorage;
}
catch (IOException ex)
{
return null;
}
}
public bool SupprimerFichier(string nomFichier)
{
bool resultat = false;
if (!String.IsNullOrEmpty(nomFichier))
{
string cheminFichier = Path.Combine(_dossierStorage, nomFichier);
if (File.Exists(cheminFichier))
{
try
{
File.Delete(cheminFichier);
resultat = true;
}
catch (Exception ex) { }
}
}
return resultat;
}
}
Ajouter ServiceCollectionExtensions
sous Shared/FileStorage
using Microsoft.Extensions.DependencyInjection;
namespace Snowfall.Shared.FileStorage;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AjouterFileStorage(this IServiceCollection services, string chemin)
{
return services.AddScoped<IFileStorage, FileStorage>(sp => new FileStorage(chemin));
}
}
Utiliser l'extension pour ajouter la "librairie" FileStorage
dans Program.cs
//...
// Ajoute le FileStorage
builder.Services.AjouterFileStorage(
Path.GetFullPath(
Path.Combine(builder.Environment.ContentRootPath,
builder.Configuration["DossierStorage"]!)));
//...
Modifier UploadsController
pour utiliser FileStorage
Vous pouvez retirer les dépendances injectées et remplacer par IFileStorage
.
public class UploadsController : ControllerBase
{
private readonly IFileStorage _fileStorage;
public UploadsController(IFileStorage fileStorage)
{
_fileStorage = fileStorage;
}
//...
Puis, la fonction Create
peut être allégée pour utiliser FileStorage
plutôt qu'un lien et sauvegarde manuelle vers le dossier Storage.
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);
// Le service FileStorage est utilisé pour sauvegarder le fichier
var nomFichierStorage = await _fileStorage.SauvegarderFichier(file);
if (nomFichierStorage is null)
{
resultatUpload.Erreur = ErreurUpload.Ecriture;
return BadRequest(resultatUpload);
}
// Compléter le DTO
resultatUpload.EstSucces = true;
resultatUpload.NomFichier = nomFichierStorage;
resultatUpload.UrlFichier = $"{baseResourceUrl}storage/{nomFichierStorage}";
// Retour 201 Created avec le DTO
return Created(resultatUpload.UrlFichier!, resultatUpload);
}
Modifier EvenementsController
pour utiliser FileStorage
Tout comme UploadsController
, FileUpload
peut être injecté dans EvenementsController
pour être utilisé afin de supprimer le fichier inutilisé lors de la suppression ou de la modification.
public class EvenementsController : ControllerBase
{
private readonly IEvenementService _evenementService;
private readonly IFileStorage _fileStorage;
private readonly IMapper _mapper;
public EvenementsController(
IEvenementService evenementService,
IFileStorage fileStorage,
IMapper mapper)
{
_evenementService = evenementService;
_fileStorage = fileStorage;
_mapper = mapper;
}
//...
Supprimer le fichier lors de la suppression d'un événement
Lors de la suppression d'un événement, vous pouvez supprimer le fichier qui y était associé. Vous devrez modifier un peu la fonction pour récupérer l'événement afin de le supprimer (pour connaitre le nom du fichier à supprimer).
On s'assure aussi de supprimer le fichier seulement si la suppression de l'événement a été un succès!
public async Task<IActionResult> Delete(int id)
{
var evenement = await _evenementService.FindById(id);
if (evenement is null)
return NotFound();
bool resultat = await _evenementService.Delete(id);
if (resultat && !String.IsNullOrEmpty(evenement.ImagePath))
_fileStorage.SupprimerFichier(evenement.ImagePath);
return resultat ? Ok() : NotFound();
}
Supprimer le fichier lors de la modification
Si le fichier associé à un événement est modifié, on voudra supprimer l'ancienne version.
Pour ce faire, on doit récupérer l'événement avant de le modifier et si le fichier a changé, on supprime l'ancien fichier.
public async Task<IActionResult> Update(int id, ModifierEvenementDto modifierEvenementDto)
{
var evenement = await _evenementService.FindById(id);
if (evenement == null)
return NotFound();
string oldImagePath = String.Empty;
if (evenement.ImagePath != null &&
modifierEvenementDto.ImagePath != evenement.ImagePath)
{
oldImagePath = evenement.ImagePath;
}
modifierEvenementDto.ApplyTo(evenement);
bool updated = await _evenementService.Update(evenement);
if (!updated)
return UnprocessableEntity();
if (!String.IsNullOrEmpty(oldImagePath))
{
_fileStorage.SupprimerFichier(oldImagePath);
}
return Ok(_mapper.Map<EvenementDto>(evenement));
}