Aller au contenu principal

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

  1. Sous le projet Shared -> Manage NuGet Packages
  2. Rechercher Microsoft.AspNetCore.Http.Abstractions
  3. Installer Microsoft.AspNetCore.Http.Abstractions dans le projet Shared

Service FileStorage

  1. Sous le projet Shared -> Add -> Directory
  2. Nommer le dossier FileStorage

Ajouter IFileStorage

Sous Shared/FileStorage, ajoutez une Interface IFileStorage:

Snowfall.Shared/FileStorage/IFileStorage.cs
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:

Snowfall.Shared/FileStorage/FileStorage.cs
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

Snowfall.Shared/FileStorage/ServiceCollectionExtensions.cs
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

Snowfall.Web.Api/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.

Snowfall.Web.Api/Controllers/UploadsController.cs
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.

Snowfall.Web.Api/Controllers/EvenementsController.cs
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!

Snowfall.Web.Api/Controllers/EvenementsController.cs
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.

Snowfall.Web.Api/Controllers/EvenementsController.cs
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));
}

Imgur