Renvoyer un fichier à afficher / télécharger dans ASP.NET MVC

304

Je rencontre un problème lors de l'envoi de fichiers stockés dans une base de données à l'utilisateur dans ASP.NET MVC. Ce que je veux, c'est une vue répertoriant deux liens, l'un pour afficher le fichier et laisser le mimetype envoyé au navigateur déterminer comment il doit être géré, et l'autre pour forcer un téléchargement.

Si je choisis d'afficher un fichier appelé SomeRandomFile.baket que le navigateur n'a pas de programme associé pour ouvrir des fichiers de ce type, je n'ai aucun problème avec le défaut de comportement de téléchargement. Cependant, si je choisis d'afficher un fichier appelé SomeRandomFile.pdfou si SomeRandomFile.jpgje souhaite que le fichier s'ouvre simplement. Mais je veux également garder un lien de téléchargement sur le côté afin de pouvoir forcer une invite de téléchargement quel que soit le type de fichier. Est-ce que ça a du sens?

J'ai essayé FileStreamResultet cela fonctionne pour la plupart des fichiers, son constructeur n'accepte pas de nom de fichier par défaut, donc les fichiers inconnus se voient attribuer un nom de fichier basé sur l'URL (qui ne connaît pas l'extension à donner en fonction du type de contenu). Si je force le nom du fichier en le spécifiant, je perds la possibilité pour le navigateur d'ouvrir le fichier directement et j'obtiens une invite de téléchargement. Quelqu'un d'autre a-t-il rencontré cela?

Ce sont les exemples de ce que j'ai essayé jusqu'à présent.

//Gives me a download prompt.
return File(document.Data, document.ContentType, document.Name);

//Opens if it is a known extension type, downloads otherwise (download has bogus name and missing extension)
return new FileStreamResult(new MemoryStream(document.Data), document.ContentType);

//Gives me a download prompt (lose the ability to open by default if known type)
return new FileStreamResult(new MemoryStream(document.Data), document.ContentType) {FileDownloadName = document.Name};

Aucune suggestion?


MISE À JOUR: Cette question semble toucher beaucoup de monde, alors j'ai pensé publier une mise à jour. L'avertissement sur la réponse acceptée ci-dessous qui a été ajouté par Oskar concernant les caractères internationaux est complètement valide, et je l'ai frappé plusieurs fois en raison de l'utilisation de la ContentDispositionclasse. J'ai depuis mis à jour mon implémentation pour résoudre ce problème. Bien que le code ci-dessous provienne de ma dernière incarnation de ce problème dans une application ASP.NET Core (Full Framework), il devrait également fonctionner avec des modifications minimales dans une ancienne application MVC puisque j'utilise la System.Net.Http.Headers.ContentDispositionHeaderValueclasse.

using System.Net.Http.Headers;

public IActionResult Download()
{
    Document document = ... //Obtain document from database context

    //"attachment" means always prompt the user to download
    //"inline" means let the browser try and handle it
    var cd = new ContentDispositionHeaderValue("attachment")
    {
        FileNameStar = document.FileName
    };
    Response.Headers.Add(HeaderNames.ContentDisposition, cd.ToString());

    return File(document.Data, document.ContentType);
}

// an entity class for the document in my database 
public class Document
{
    public string FileName { get; set; }
    public string ContentType { get; set; }
    public byte[] Data { get; set; }
    //Other properties left out for brevity
}
Nick Albrecht
la source

Réponses:

430
public ActionResult Download()
{
    var document = ...
    var cd = new System.Net.Mime.ContentDisposition
    {
        // for example foo.bak
        FileName = document.FileName, 

        // always prompt the user for downloading, set to true if you want 
        // the browser to try to show the file inline
        Inline = false, 
    };
    Response.AppendHeader("Content-Disposition", cd.ToString());
    return File(document.Data, document.ContentType);
}

REMARQUE: cet exemple de code ci-dessus ne prend pas correctement en compte les caractères internationaux dans le nom de fichier. Voir RFC6266 pour la normalisation pertinente. Je crois que les versions récentes de la File()méthode ASP.Net MVC et la ContentDispositionHeaderValueclasse en tiennent correctement compte. - Oskar 2016-02-25

Darin Dimitrov
la source
7
Si je me souviens bien, il peut être non cité tant que le nom de fichier n'a pas d'espaces (j'ai exécuté le mien via HttpUtility.UrlEncode () pour y parvenir).
Keith Williams
22
Remarque: Si vous utilisez ceci et définissez Inline = trueassurez-vous de NE PAS utiliser la surcharge à 3 paramètres File()qui prend le nom de fichier comme 3e paramètre. Il va travailler dans IE, mais Chrome signale un en- tête en double et refuser de présenter l'image.
Faust
74
De quel type est var document = ...?
TTT
7
@ user1103990, c'est votre modèle de domaine.
Darin Dimitrov
3
En utilisant MVC 5, il n'est plus nécessaire d'en-tête de disposition de contenu, car il fait déjà partie de l'en-tête de réponse. Mais je ne reçois qu'une boîte de dialogue de téléchargement dans FF, pas de boîte de dialogue dans Chrome et IE
Legends
124

J'ai eu des problèmes avec la réponse acceptée car aucun type ne faisait allusion à la variable "document": var document = ...je publie donc ce qui a fonctionné pour moi au cas où quelqu'un d'autre aurait des problèmes.

public ActionResult DownloadFile()
{
    string filename = "File.pdf";
    string filepath = AppDomain.CurrentDomain.BaseDirectory + "/Path/To/File/" + filename;
    byte[] filedata = System.IO.File.ReadAllBytes(filepath);
    string contentType = MimeMapping.GetMimeMapping(filepath);

    var cd = new System.Net.Mime.ContentDisposition
    {
        FileName = filename,
        Inline = true,
    };

    Response.AppendHeader("Content-Disposition", cd.ToString());

    return File(filedata, contentType);
}
ooXei1sh
la source
4
La variable document était juste une classe (POCO) représentant les informations sur le document que vous vouliez retourner. Cela a également été demandé dans la réponse acceptée. Il peut provenir d'un ORM, d'une requête SQL construite manuellement, du système de fichiers (comme le vôtre tire des informations) ou d'un autre magasin de données. Ce n'était pas pertinent pour la question d'origine d'où venaient vos octets de document / nom de fichier / type MIME, donc il a été laissé de côté pour ne pas brouiller le code. Merci d'avoir fourni un exemple en utilisant uniquement le système de fichiers.
Nick Albrecht
1
Cette solution ne fonctionne pas correctement lorsque le nom de fichier contient des caractères internationaux en dehors de US-ASCII.
Oskar Berggren
1
Merci, J'ai sauvé ma journée :)
Dipesh
1
Une alternative à AppDomain.CurrentDomain.BaseDirectoryest System.Web.HttpContext.Current.Server.MapPath("~"), cela peut mieux fonctionner sur un vrai serveur par rapport à une machine locale.
Chris Thompson
15

La réponse de Darin Dimitrov est correcte. Juste un ajout:

Response.AppendHeader("Content-Disposition", cd.ToString());peut entraîner l'échec du rendu du fichier par le navigateur si votre réponse contient déjà un en-tête "Content-Disposition". Dans ce cas, vous pouvez utiliser:

Response.Headers.Add("Content-Disposition", cd.ToString());
Leo
la source
Je trouve que le type de contenu a également une influence, pour le pdffichier, si je définit le type de contenu comme System.Net.Mime.MediaTypeNames.Application.Octet, il forcera le téléchargement même lorsque je le définirai Inline = true, mais si je le définit comme Response.ContentType = MimeMapping.GetMimeMapping(filePath), c'est-à- application/pdfdire qu'il peut s'ouvrir correctement plutôt que de le télécharger
yu yang Jian
Response.Headers.Addnécessite le mode pipeline intégré IIS. De plus, même si le pool d'applications est défini comme intégré, il lèvera une exception. Solution. Utilisez Response.AddHeader. Voir le fil SO: stackoverflow.com/questions/22313167/…
roland
12

Pour visualiser le fichier (txt par exemple):

return File("~/TextFileInRootDir.txt", MediaTypeNames.Text.Plain);

Pour télécharger un fichier (txt par exemple):

return File("~/TextFileInRootDir.txt", MediaTypeNames.Text.Plain, "TextFile.txt");

note: pour télécharger le fichier, nous devons passer l' argument fileDownloadName

Ashot Muradian
la source
Le seul problème avec cette approche est que le nom de fichier n'est fourni que si vous utilisez la méthode qui force un téléchargement. Il ne me permet pas d'envoyer le nom de fichier correct lorsque je souhaite laisser le navigateur déterminer comment ouvrir le fichier. L'application externe tentera donc d'utiliser mon URL comme nom de fichier. Ce sera généralement une sorte d'ID pour le document dans la base de données, généralement sans extension de fichier. Cela fait un assez mauvais le nom ne correspond pas à l'URL utilisée pour accéder au fichier, car le scénario est que si vous ne fournissez pas.
Nick Albrecht
L'utilisation de la Inlinepropriété sur Content-Disposition me permet de séparer la possibilité de définir le nom de fichier du comportement de forcer le téléchargement ou non.
Nick Albrecht
4

Je crois que cette réponse est plus propre, (basée sur https://stackoverflow.com/a/3007668/550975 )

    public ActionResult GetAttachment(long id)
    {
        FileAttachment attachment;
        using (var db = new TheContext())
        {
            attachment = db.FileAttachments.FirstOrDefault(x => x.Id == id);
        }

        return File(attachment.FileData, "application/force-download", Path.GetFileName(attachment.FileName));
    }
Serj Sagan
la source
9
Non recommandé, Content-Disposition est la méthode préférée pour la clarté et la compatibilité. stackoverflow.com/questions/10615797/…
Nick Albrecht
Merci pour ça. Bien que j'ai changé le type MIME en application/octet-streamet cela a toujours causé le téléchargement du fichier plutôt que son affichage, et il semble être compatible.
Serj Sagan
1
Mentir sur le type de contenu semble être une très mauvaise idée. Certains navigateurs dépendent du type de contenu correct pour suggérer des applications pour l'utilisateur dans la boîte de dialogue "enregistrer ou ouvrir".
Oskar Berggren
Si votre intention est que le navigateur suggère une application, c'est bien, mais cette question concerne spécifiquement le forçage du téléchargement ...
Serj Sagan
@SerjSagan Je pense qu'il s'agit davantage de contourner le comportement par défaut des navigateurs pour afficher certains types de fichiers directement ou d'utiliser un plugin, au lieu d'offrir le choix entre enregistrer / ouvrir. Je n'ai pas essayé par exemple avec JPEG pour le moment, donc je ne suis pas sûr du comportement exact.
Oskar Berggren
3

FileVirtualPath -> Research \ Global Office Review.pdf

public virtual ActionResult GetFile()
{
    return File(FileVirtualPath, "application/force-download", Path.GetFileName(FileVirtualPath));
}
Bishoy Hanna
la source
5
Cela a déjà été mentionné dans la réponse précédente et n'est pas recommandé. Voir la question suivante pour plus de détails sur pourquoi. stackoverflow.com/questions/10615797/…
Nick Albrecht
1
De cette façon, il n'utilise pas les ressources sur le serveur en chargeant le fichier en mémoire sur le serveur, ai-je raison?
Ian Jowett
1
Je crois que oui, car pas besoin de vous à droite @CrashOverride
Bishoy Hanna
1

Le code ci-dessous a fonctionné pour moi pour obtenir un fichier pdf à partir d'un service API et le répondre au navigateur - j'espère que cela aide;

public async Task<FileResult> PrintPdfStatements(string fileName)
    {
         var fileContent = await GetFileStreamAsync(fileName);
         var fileContentBytes = ((MemoryStream)fileContent).ToArray();
         return File(fileContentBytes, System.Net.Mime.MediaTypeNames.Application.Pdf);
    }
Jonny Boy
la source
2
Merci d'avoir répondu. Vous avez manqué la partie où je cherchais une solution qui incluait la possibilité de spécifier le nom du fichier. Vous renvoyez simplement des octets, le nom serait donc déduit de l'URL. J'utilisais PDF comme exemple, mais j'en avais besoin pour fonctionner pour plusieurs autres types de fichiers aussi dans mon cas. Je dois mentionner que votre solution fonctionnerait tant que vous utilisez le .NET Framework 4.x et MVC <= 5. Si vous utilisez contre .NET Core, votre meilleur pari est d'utiliserMicrosoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider
Nick Albrecht
@NickAlbrecht Je n'ai pas utilisé .Net Core - l'exemple ci-dessus était pour la réponse pdf sur un navigateur. Cependant, si vous souhaitez télécharger, essayez: Fichier (fileContentBytes, System.Net.Mime.MediaTypeNames.Application.Pdf, "votre nom de fichier"). Je ne sais pas si vous avez obtenu votre réponse. s'il vous plaît laissez-moi savoir si cela aide.Merci pour la suggestion .Net Core cependant. De plus, si vous avez trouvé ma réponse utile, veuillez ajouter un vote.
Jonny Boy
J'ai déjà résolu le problème il y a longtemps avec la réponse que j'ai marquée comme acceptée. J'évoquais plus quelques mises en garde au cas où vous les trouveriez utiles. Ma question d'origine était 2011, donc c'est assez daté maintenant.
Nick Albrecht
@NickAlbrecht Merci. Je ne savais pas que vous l'aviez résolu, j'ai vu que c'était un très vieux message. J'ai trouvé ce forum en recherchant des réponses liées à async, je suis nouveau dans les processus async. j'ai pu résoudre mon problème alors je viens de le partager. Que vous pour votre temps.
Jonny Boy
0

La méthode d'action doit renvoyer FileResult avec un flux, un octet [] ou un chemin virtuel du fichier. Vous devrez également connaître le type de contenu du fichier en cours de téléchargement. Voici un exemple de méthode utilitaire (rapide / sale). Exemple de lien vidéo Comment télécharger des fichiers à l'aide du noyau asp.net

[Route("api/[controller]")]
public class DownloadController : Controller
{
    [HttpGet]
    public async Task<IActionResult> Download()
    {
        var path = @"C:\Vetrivel\winforms.png";
        var memory = new MemoryStream();
        using (var stream = new FileStream(path, FileMode.Open))
        {
            await stream.CopyToAsync(memory);
        }
        memory.Position = 0;
        var ext = Path.GetExtension(path).ToLowerInvariant();
        return File(memory, GetMimeTypes()[ext], Path.GetFileName(path));
    }

    private Dictionary<string, string> GetMimeTypes()
    {
        return new Dictionary<string, string>
        {
            {".txt", "text/plain"},
            {".pdf", "application/pdf"},
            {".doc", "application/vnd.ms-word"},
            {".docx", "application/vnd.ms-word"},
            {".png", "image/png"},
            {".jpg", "image/jpeg"},
            ...
        };
    }
}
VETRIVEL D
la source
Un lien vers quelque chose auquel vous êtes affilié (par exemple, une bibliothèque, un outil, un produit, un didacticiel ou un site Web) sans divulguer qu'il vous appartient est considéré comme du spam sur Stack Overflow. Voir: Qu'est - ce qui signifie une "bonne" promotion personnelle? , quelques astuces et conseils sur l'autopromotion , Quelle est la définition exacte du "spam" pour Stack Overflow? et ce qui fait du spam .
Samuel Liew
0

Si, comme moi, vous avez abordé ce sujet via les composants Razor pendant que vous apprenez Blazor, vous constaterez que vous devez réfléchir un peu plus en dehors de la boîte pour résoudre ce problème. C'est un peu un champ de mines si (également comme moi) Blazor est votre première incursion dans le monde de type MVC, car la documentation n'est pas aussi utile pour de telles tâches `` subalternes ''.

Donc, au moment de la rédaction, vous ne pouvez pas y parvenir en utilisant vanilla Blazor / Razor sans incorporer un contrôleur MVC pour gérer la partie de téléchargement de fichier dont un exemple est comme ci-dessous:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;

[Route("api/[controller]")]
[ApiController]
public class FileHandlingController : ControllerBase
{
    [HttpGet]
    public FileContentResult Download(int attachmentId)
    {
        TaskAttachment taskFile = null;

        if (attachmentId > 0)
        {
            // taskFile = <your code to get the file>
            // which assumes it's an object with relevant properties as required below

            if (taskFile != null)
            {
                var cd = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
                {
                    FileNameStar = taskFile.Filename
                };

                Response.Headers.Add(HeaderNames.ContentDisposition, cd.ToString());
            }
        }

        return new FileContentResult(taskFile?.FileData, taskFile?.FileContentType);
    }
}

Ensuite, assurez-vous que le démarrage de votre application (Startup.cs) est configuré pour utiliser correctement MVC et que la ligne suivante est présente (ajoutez-la sinon):

        services.AddMvc();

.. et enfin modifier votre composant pour le lier au contrôleur, par exemple (exemple basé sur des itérations utilisant une classe personnalisée):

    <tbody>
        @foreach (var attachment in yourAttachments)
        {
        <tr>
            <td><a href="api/[email protected]" target="_blank">@attachment.Filename</a> </td>
            <td>@attachment.CreatedUser</td>
            <td>@attachment.Created?.ToString("dd MMM yyyy")</td>
            <td><ul><li class="oi oi-circle-x delete-attachment"></li></ul></td>
        </tr>
        }
        </tbody>

J'espère que cela aidera quiconque a eu du mal (comme moi!) À obtenir une réponse appropriée à cette question apparemment simple dans le royaume de Blazor…!

VorTechS
la source
Bien que cela puisse être utile à d'autres personnes rencontrant le même problème que vous, il serait plus facile de les découvrir si vous avez posté votre propre question avec un titre qui indique qu'elle est propre à Blazor, répondez-y vous-même et ajoutez un commentaire suggérant ici quiconque atteindra cette question avec des problèmes concernant blazor consultez votre lien. Je sentais que j'atteignais même avec cette question d'origine étant pour ASP.NET MVC, et en adaptant sa réponse pour qu'elle soit pertinente pour ASP.NET Core. Blazor est une bête totalement différente, comme vous l'avez découvert.
Nick Albrecht