Une meilleure gestion du contrôle serveur FileUpload

Parmi les contrôles serveurs fournis par ASP.NET, on a à notre disposition le dénommé FileUpload, qui permet à l’utilisateur d’enregistrer (uploader) un fichier sur le serveur.

Seulement, ce contrôle serveur manque de clarté en ce qui concerne la taille maximale d’un fichier autorisée. En effet, d’une part, cette limite est faible par défaut (4 Mo), et d’autre part, les messages d’erreur ou d’avertissement sont peu explicites, ni pour l’utilisateur, ni pour le développeur. Par exemple, sous Firefox, on obtient le message suivant :

La connexion a été réinitialisée
La connexion avec le serveur a été réinitialisée pendant le chargement de la page.
...

Nous allons donc voir comment maîtriser d’avantage la taille limite des fichiers à uploader et comment en avertir l’utilisateur.

Mise en situation

Pour illustrer le sujet, il suffit de créer une page ASPX, avec un FileUpload, un Button et un Label :

<asp:FileUpload ID="fileUploadControl" runat="server" />
<asp:Button ID="btnUpload" runat="server" OnClick="btnUpload_Click" Text="Valider" />
<asp:Label ID="lblFileUploadResult" runat="server"></asp:Label>

On implémente ensuite l’action qui découle du clique sur le bouton. Cette action consiste simplement à enregistrer le fichier sur le serveur et à afficher un message récapitulatif :

protected void btnUpload_Click(object sender, EventArgs e)
{
    if (fileUploadControl.HasFile)
    {
        fileUploadControl.SaveAs(@"C:\Temp\Uploads\" + fileUploadControl.FileName) ; // Attention à bien créer le répertoire cible
        lblFileUploadResult.Text = "Nom du fichier enregistré : " + fileUploadControl.FileName;
    }
    else
    {
        lblFileUploadResult.Text = "Aucun fichier n'a été enregistré.";
    }
}

On lance ensuite l’application Web, on choisit un fichier de moins de 4 Mo, et on clique sur le bouton d’enregistrement. Tout devrait être fonctionnel (sauf si le répertoire de destination n’existe pas, par exemple). Maintenant, on teste à nouveau avec un fichier avec un fichier de plus de 4 Mo. Stupeur ! Le navigateur afficher un message d’erreur, a priori peu explicite, même pour le développeur. En mode « debug », on voit que le processus ne passe même pas dans la fonction « OnClick » du bouton…

L’Observateur d’événements de Windows indique néanmoins quelques messages intéressants :

Event code: 3004
Event message: La taille du postage a dépassé les limites autorisées.
Exception information:
Exception type: HttpException
Exception message: Longueur maximale de la demande dépassée.

Nous allons donc chercher à changer cette taille limite, à avertir l’utilisateur en cas de dépassement, et à maîtriser l’erreur HttpException.

Changer la taille maximum des fichiers dans le Web.config

On peut en effet voir cette limite dans le fichier Web.config.comments (C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG), dans la section « httpRuntime », à l’entrée « maxRequestLength ». Cette valeur est de 4096ko, soit 4Mo. On peut modifier ici cette valeur, ou bien dans le fichier Web.config situé à la racine de l’application Web (ce qui est recommandé).

Voici comment changer cette limite à 8Mo, via le Web.config de l’application. Il faut veiller à inclure ce paramètre dans le nœud « system.web ».

<system.web>
   <httpRuntime maxRequestLength="8192"></httpRuntime>
   [...]
</system.web>

Mettre un validateur sur la taille du fichier

Néanmoins, si un utilisateur souhaite uploader un fichier de plus de 8Mo, il obtiendra toujours la même erreur, peu explicite. Nous allons donc mettre en place un validateur, avec un message précis, indiquant que la taille du fichier uploadé est trop élevée.

Tout d’abord, il faut augmenter la valeur de maxRequestLength (exemple : maxRequestLength="20000", soit un peu moins de 20Mo), ce qui permettra d’avoir de la marge entre la taille maximale autorisée sur le serveur (source de l'erreur HttpException), et celle autorisée par le validateur.

Il faut ensuite insérer le validateur dans la page ASPX :

<asp:CustomValidator ID="customValidatorMaxSize" runat="server" ControlToValidate="fileUploadControl"
OnServerValidate="customValidatorMaxSize_ServerValidate" Display="Dynamic" ValidateEmptyText="true" />

Puis, ajouter le code associé au validateur :

protected void customValidatorMaxSize_ServerValidate(object sender, ServerValidateEventArgs args)
{
    int tailleMax = 8192; // Taille max en kilo-octets, à mettre dans une variable du Web.config, ou en paramètre du validateur, ou à lire directement depuis le httpRuntime du Web.config
    int tailleTotale = 0;
    HttpFileCollection hfc = Request.Files;

    // FileUpload ne permet d'uploader qu'un seul fichier, mais la boucle suivante permet de gérer un contrôle serveur personnalisé permettant le multi-upload
    for (int i = 0; i < hfc.Count; i++)
    {
        HttpPostedFile hpf = hfc[i];
        tailleTotale += (hpf.ContentLength / 1024); // ContentLength donne la taille en octets
    }

    if (tailleTotale >= tailleMax)
    {
        customValidatorMaxSize.Text = "Le fichier ne doit pas dépasser la taille de " + Math.Round(tailleMax / 1024.0, 2) + "Mb";
        args.IsValid = false;
    }
    else
        args.IsValid = true;
}

Dernière chose, il faut ajouter un test de validité de la page dans le code d’enregistrement du fichier :

protected void btnUpload_Click(object sender, EventArgs e)
{
if (fileUploadControl.HasFile && Page.IsValid)
[...]

Mais nous ne sommes pas au bout de nos peines, puisque si l’utilisateur tente d’uploader un fichier dépassant la limite indiquée par maxRequestLength (20Mo), il obtiendra toujours la même erreur HttpException…

Attraper l’erreur HttpException

En effet, l’erreur qui nous concerne depuis le début de cet article est une exception HTTP lancée au moment de l’écriture du fichier uploadé, avant d’être inséré dans l’objet Request, et donc avant d’être lisible dans le code du validateur.

On peut donc dans un premier temps « tricher » en augmentant la valeur de maxRequestLength au maximum, en l’occurrence 2097151, soit un peu moins de 2Go. Cela laisse de la marge. Mais il existe néanmoins un moyen d’attraper l’exception lancée, et ainsi de rediriger par exemple l’utilisateur vers une page d’erreur, ou de logger l’erreur (côté développeur).

Il faut créer une classe qui implémente l’interface IHttpModule chargée d’intercepter la requête.

namespace BlogUpload
{
    public class CatchFileErrorModule : IHttpModule
    {
        public void Init(HttpApplication app)
        {
            app.BeginRequest += new EventHandler(app_BeginRequest);
        }

        void app_BeginRequest(object sender, EventArgs e)
        {
            HttpContext context = ((HttpApplication)sender).Context;

            // On va lire la taille maximale autorisée par le serveur dans le Web.config
            System.Configuration.Configuration config = WebConfigurationManager.OpenWebConfiguration("~");
            HttpRuntimeSection section = config.GetSection("system.web/httpRuntime") as HttpRuntimeSection;
            int maxFilesSize = section.MaxRequestLength;

            // Si la taille du fichier est supérieure à la taille indiquée par httpRuntime
            if ((context.Request.ContentLength / 1024) > maxFilesSize)
            {
                // On vide le contenu de la requête avec un système de tampon
                IServiceProvider provider = (IServiceProvider)context;
                HttpWorkerRequest wr = (HttpWorkerRequest)provider.GetService(typeof(HttpWorkerRequest));
                if (wr.HasEntityBody())
                {
                    int requestLength = wr.GetTotalEntityBodyLength();
                    int initialBytes = wr.GetPreloadedEntityBody().Length;
                    if (!wr.IsEntireEntityBodyIsPreloaded())
                    {
                        byte[] buffer = new byte[512000];
                        int receivedBytes = initialBytes;
                        while ((requestLength - receivedBytes) >= initialBytes)
                        {
                            initialBytes = wr.ReadEntityBody(buffer, buffer.Length);
                            receivedBytes += initialBytes;
                        }
                        initialBytes = wr.ReadEntityBody(buffer, (requestLength - receivedBytes));
                    }
                }

                // On redirige l'utilisateur vers une page d'erreur, ou sur la page courrante avec un message d'avertissement, ou autre...
                context.Response.Redirect("~/Default.aspx");
            }
        }

        public void Dispose()
        {

        }
    }
}

Puis déclarer cette classe dans le Web.config, dans la section « system.web ».

<httpModules>
  <add type="BlogUpload.CatchFileErrorModule" name="CatchFileErrorModule"/>
</httpModules>

Merci aux bloggeurs suivants pour l’inspiration. Ce sont de bons exemples où l’on peut voir que l’upload peut également être directement gérée directement dans la classe.

Conclusion

Le contrôle serveur FileUpload est donc étonnement difficile à gérer par défaut. On peut espérer que Microsoft propose quelque chose de plus ergonomique dans les futures versions de ses Framework ou dans des bibliothèques additionnelles. C’est peut-être déjà le cas. N’hésitez pas à vous exprimer dans les commentaires si vous avez des précisions à ce sujet.

Mais un développeur peut parfois être limité en terme de ressources et doit donc s’adapter aux outils mis à sa disposition (le Framework 2.0 ici). J’espère donc que cet article sera un bon point de départ à une gestion plus robuste de l’upload de fichiers pour votre site.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.