Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Faire survivre une sélection de fichiers à un upload

Poster un commentaire

Vous avez une appli qui ressemble à ceci:

On aperçoit 3 <input type= »file »/>. Disons que la règle métier est que l’on doit obligatoirement fournir TROIS fichiers. Seulement je n’en ai fournit qu’un seul.

Si je clique sur SUBMIT. La validation échoue, je reviens sur la page avec un message d’erreur.

A votre avis la sélection initiale sera t’elle perdue? Si oui, que peut t’on faire pour l’empêcher?

Ben oui, par défaut rien n’est conservé et on obtient ceci:

Côté vue le point de départ sera celui-ci:


<form method="POST" asp-action="Upload" enctype="multipart/form-data">
   <input type="file" name="file1" /><br />
   <input type="file" name="file2" /><br />
   <input type="file" name="file3" /><br />
   <hr />
   <input type="submit" value="Submit" />

   @Html.ValidationSummary()
</form>

Côté contrôleur:


public IActionResult Index()
{
return View();
}

[HttpPost]
public IActionResult Upload(IFormFile file1, IFormFile file2, IFormFile file3)
{
   if (file1 == null || file2 == null || file3 == null)
   {
      ModelState.AddModelError("file", "Oh le boulet!");
   }

   if (ModelState.IsValid)
   {
      // fait des trucs cools ici
      // ...

      return RedirectToAction("Index");
   }

   return View("Index");
}

Le contrôleur est un peu spécifique à Core, mais c’est le seul truc dans cet article. La partie intéressante est générique.

Comment assurer la survie de la sélection?

Bien entendu on ne peut pas renvoyer le fichier reçu vers le poste client pour réalimenter le <input>. De toute façon il est impossible d’écrire via le code JavaScript dans un <input type= »file/> pour des raisons de sécurité. Il faut se débrouiller autrement.

 

La solution que je propose est assez simple:

  • On sauvegarde localement le ou les fichiers déjà reçus
  • on sauvegarde localement les infos relatives à ce fichier (type média, nom…)
  • On réaffiche les 3 inputs
  • Mais lorsqu’un des <input> correspond à un fichier précédemment sélectionné on ajoute en plus un petit champ texte avec le nom du fichier.
    On montre ainsi qu’une sélection existe déjà et n’est pas perdue
  • Si un fichier différent est chargé à la place de l’existant ce dernier sera aussi écrasé

Assez simple sur le principe, mais il faut être précis. Par exemple si j’ai sélectionné un fichier sur le <input> du milieu, c’est lui qui doit être restauré avec le bon nom de fichier. On ne joue pas au bonneteau.

Côté contrôleur

On a tout d’abord besoin d’un vue/modèle appelé FileViewModel définit ainsi:


public class FileViewModel
{
public string ContextFile
{
   get; set;
}
public string Path
{
   get; set;
}

public string Media1
{
   get; set;
}

public string FileName1
{
   get; set;
}
public string Media2
{
   get; set;
}

public string FileName2
{
   get; set;
}
public string Media3
{
   get; set;
}

public string FileName3
{
   get; set;
}
}

Ce vue/modèle va servir à alimenter la persistance du formulaire et faire fonctionner la plomberie de persistance de la sélection des fichiers. C’est pourquoi on y trouve des propriétés des deux contextes.

On enregistre le nom des fichiers dans les propriétés FilenameX et le type de média dans les propriétés MediaX. Ce dernier paramètre est facile à obtenir directement depuis la requête et plus pénible après coup, donc autant les enregistrer dès maintenant.

Path et ContextFile seront détaillés plus loin.

 

Ouvrons le contrôleur.

On doit maintenant retourner un FileViewModel dans toutes les requêtes, donc:


public IActionResult Index()
{
   return View(new FileViewModel());
}

La méthode d’action intéressante est bien entendu Upload qui devient:


[HttpPost]
public async Task<IActionResult> Upload(IFormFile file1, IFormFile file2, IFormFile file3, string path)
{
   if (file1 == null || file2 == null || file3 == null)
   {
      ModelState.AddModelError("file", "Oh le boulet!");
   }

   if (ModelState.IsValid)
   {
      // fait des trucs cools ici
      // ...

      FileViewModel vm1 = await LoadParams(file1, file2, file3, path);
      //Save(vm1);

      return RedirectToAction("Index");
   }

   // le modèle n'est pas validé
   FileViewModel vm = await LoadParams(file1, file2, file3, path);

   string json = JsonConvert.SerializeObject(vm);
   System.IO.File.WriteAllText(vm.ContextFile, json);

   return View("Index", vm);
}

On commence par valider que 3 fichiers sont envoyés et au besoin on alimente en conséquence ModelState.

Il y a ensuite 2 cas:

  1. La validation réussie (lignes 11 à 17)
  2. La validation échoue (ligne > 20)

Traitons le deuxième cas. On appelle une méthode LoadParams qui retourne un FileViewModel. LoadParams implémente la plomberie.

Les lignes 22 et 23 sont importantes. Je sérialise FileViewModel et l’enregistre dans un fichier dont le nom est donné par ContextFile.

Ce fichier est sauvegardé dans un répertoire qui contient les fichiers éventuellement envoyés. C’est le cœur de notre algo.

Il est urgent d’ouvrir LoadParams.


private async Task<FileViewModel> LoadParams(IFormFile file1, IFormFile file2, IFormFile file3, string path)
{
   FileViewModel vm = new FileViewModel();
   if (!string.IsNullOrEmpty(path))
   {
      string vmString = System.IO.File.ReadAllText(path);
      vm = JsonConvert.DeserializeObject<FileViewModel>(vmString);
   }
   else
   {
      vm.Path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
      Directory.CreateDirectory(vm.Path);
      vm.ContextFile = Path.Combine(vm.Path, "context.json");
   }

   if (file1 != null)
   {
      vm.FileName1 = Path.GetFileName(file1.FileName);
      vm.Media1 = Path.GetFileName(file1.ContentType);

      await Write(vm.Path, vm.FileName1, file1);
   }
   if (file2 != null)
   {
      vm.FileName2 = Path.GetFileName(file2.FileName);
      vm.Media2 = Path.GetFileName(file2.ContentType);

      await Write(vm.Path, vm.FileName2, file2);
   }
   if (file3 != null)
   {
      vm.FileName3 = Path.GetFileName(file3.FileName);
      vm.Media3 = Path.GetFileName(file3.ContentType);

      await Write(vm.Path, vm.FileName3, file3);
   }

   return vm;
}

private async Task Write(string targetPath, string fileName, IFormFile file)
{
   string filename = Path.Combine(targetPath, fileName);
   using (FileStream stream = new FileStream(filename, FileMode.Create))
   {
      await file.CopyToAsync(stream);
   }
}

Les ligne 3-14 initialisent un nouveau FileViewModel ou charge celui qui existe déjà pour le compléter. La sauvegarde a lieu dans un répertoire temporaire avec un nom privatisé. Le fichier s’appelle secret.json.

Les lignes suivantes sauvegardent dans notre répertoire les fichiers qui ont été envoyés et met à jour le FileViewModel qui sera ré-enregistré un peu plus loin dans le contrôleur. On a donc gardé une trace locale de la situation.

Il reste à regarder rapidement le cas où la validation passe.

C’est simple, on rappelle LoadParams(). L’idée est de partir d’une situation standard où les fichiers sont tous au même endroit et non pas répartis entre la sauvegarde locale et les paramètres de la requête. On a donc un simple paramètre à passer à une hypothétique méthode Save() qui enregistre les fichiers dans un emplacement définitif comme un répertoire partagé ou une base de donnée.

 

Il ne reste plus qu’à découvrir la vue.

La vue

On a besoin d’ajouter les zones d’affichage des noms de fichier:

</pre>
@model FileViewModel
@using WebApplication2.Models

<form method="POST" asp-action="Upload" enctype="multipart/form-data">
   <input type="file" name="file1" /><br />
   <div>@Model.FileName1</div><br /><br />
   <input type="file" name="file2" /><br />
   <div>@Model.FileName2</div><br /><br />
   <input type="file" name="file3" /><br />
   <div>@Model.FileName3</div><br /><br />

   <hr />
   <input type="submit" value="Submit" />

   <input type="hidden" name="path" value="@Model.ContextFile" />

   @Html.ValidationSummary()
</form>
<pre>

La vue reçoit un modèle qui permet d’alimenter des <div> contenant le nom des fichiers déjà enregistré localement.

Si on lance:

J’ai un peu espacé les <input> pour faire plus propre.

 

On fait une sélection:

Puis une deuxième:

Ca fonctionne comme prévu.

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion /  Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion /  Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion /  Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion /  Changer )

w

Connexion à %s