Retornando um arquivo para exibir / baixar no asp.net MVC

304

Estou com um problema ao enviar arquivos armazenados em um banco de dados de volta para o usuário no ASP.NET MVC. O que eu quero é uma visualização listando dois links, um para exibir o arquivo e permitir que o tipo de mim enviado ao navegador determine como ele deve ser tratado e o outro para forçar um download.

Se eu optar por exibir um arquivo chamado SomeRandomFile.bake o navegador não tiver um programa associado para abrir arquivos desse tipo, não tenho problemas com o padrão do comportamento do download. No entanto, se eu optar por exibir um arquivo chamado SomeRandomFile.pdfou SomeRandomFile.jpgdesejar que ele seja simplesmente aberto. Mas também quero manter um link de download ao lado para poder forçar um prompt de download, independentemente do tipo de arquivo. Isso faz sentido?

Eu tentei FileStreamResulte funciona para a maioria dos arquivos; seu construtor não aceita um nome de arquivo por padrão; portanto, arquivos desconhecidos recebem um nome de arquivo com base na URL (que não conhece a extensão a ser atribuída com base no tipo de conteúdo). Se forçar o nome do arquivo especificando-o, perco a capacidade do navegador de abrir o arquivo diretamente e recebo uma solicitação de download. Mais alguém encontrou isso?

Estes são os exemplos do que eu tentei até agora.

//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};

Alguma sugestão?


ATUALIZAÇÃO: Essas perguntas parecem impressionar muita gente, então pensei em publicar uma atualização. O aviso na resposta aceita abaixo, que foi adicionado por Oskar em relação aos personagens internacionais, é completamente válido, e eu o recebi algumas vezes devido ao uso da ContentDispositionclasse. Atualizei minha implementação para corrigir isso. Embora o código abaixo seja da minha versão mais recente desse problema em um aplicativo ASP.NET Core (Full Framework), ele deve funcionar com alterações mínimas em um aplicativo MVC mais antigo, desde que eu esteja usando a 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
fonte

Respostas:

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);
}

NOTA: Este código de exemplo acima falha ao considerar adequadamente caracteres internacionais no nome do arquivo. Veja RFC6266 para a padronização relevante. Acredito que versões recentes do File()método ASP.Net MVC e a ContentDispositionHeaderValueclasse sejam responsáveis ​​por isso. - Oskar 25-02-2016

Darin Dimitrov
fonte
7
Se bem me lembro, ele pode ser não citado desde que o nome do arquivo não tenha espaços (executei o meu através do HttpUtility.UrlEncode () para conseguir isso).
28512 Keith Williams
22
Nota: Se você usar isso e configurar, Inline = truecertifique-se de NÃO usar a sobrecarga de 3 parâmetros File()que leva o nome do arquivo como o 3º parâmetro. Ele vai funcionar no IE, mas o Chrome irá relatar um cabeçalho duplicado e recusar-se a apresentar a imagem.
Faust
74
Que tipo é var document = ...?
TTT
7
@ user1103990, é o seu modelo de domínio.
Darin Dimitrov
3
Usando o MVC 5, não há mais necessidade do cabeçalho de disposição do conteúdo, pois ele já faz parte do cabeçalho de resposta. Mas eu só obter um diálogo de download no FF, nenhum diálogo em cromo e IE
Legends
124

Eu tive problemas com a resposta aceita devido a nenhum tipo de sugestão na variável "document": var document = ...Portanto, estou postando o que funcionou para mim como uma alternativa no caso de alguém mais estar tendo problemas.

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
fonte
4
A variável do documento era apenas uma classe (POCO) representando as informações sobre o documento que você queria retornar. Isso foi solicitado na resposta aceita também. Pode vir de um ORM, de uma consulta SQL criada manualmente, do sistema de arquivos (conforme o seu extrai informações) ou de algum outro armazenamento de dados. Era irrelevante para a pergunta original de onde vieram os bytes / nome do arquivo / tipo mime do documento, portanto foi deixado de fora para não confundir o código. Obrigado por contribuir com um exemplo usando apenas o sistema de arquivos.
Nick Albrecht
1
Esta solução não funciona corretamente quando o nome do arquivo contém caracteres internacionais fora do US-ASCII.
Oskar Berggren
1
Obrigado, salvar meu dia :)
Dipesh
1
Uma alternativa AppDomain.CurrentDomain.BaseDirectoryé que System.Web.HttpContext.Current.Server.MapPath("~")isso funcione melhor em um servidor real comparado a uma máquina local.
21417 Chris Thompson
15

A resposta de Darin Dimitrov está correta. Apenas uma adição:

Response.AppendHeader("Content-Disposition", cd.ToString());pode causar falha no navegador ao renderizar o arquivo se sua resposta já contiver um cabeçalho "Disposição de conteúdo". Nesse caso, você pode querer usar:

Response.Headers.Add("Content-Disposition", cd.ToString());
Leo
fonte
Acho que o tipo de conteúdo também tem influência, por pdfarquivo, se eu definir o tipo de conteúdo System.Net.Mime.MediaTypeNames.Application.Octet, forçará o download mesmo quando eu definir Inline = true, mas se eu definir como Response.ContentType = MimeMapping.GetMimeMapping(filePath), ou seja application/pdf, ele poderá abrir corretamente em vez de baixar
yu yang Jian
Response.Headers.Addrequer o modo de pipeline integrado do IIS. Além disso, mesmo se o pool de aplicativos estiver definido como integrado, ele lançará uma exceção. Solução. Use Response.AddHeader. Veja SO thread: stackoverflow.com/questions/22313167/…
roland
12

Para visualizar o arquivo (txt por exemplo):

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

Para baixar o arquivo (txt por exemplo):

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

note: para baixar o arquivo, devemos passar o argumento fileDownloadName

Ashot Muradian
fonte
O único problema com essa abordagem é que o nome do arquivo será fornecido apenas se você usar o método que força um download. Ele não me permite enviar o nome correto do arquivo quando quero que o navegador determine como abrir o arquivo. Portanto, o aplicativo externo tentará usar meu URL como o nome do arquivo. Que geralmente será algum tipo de identificação do documento no banco de dados, normalmente sem uma extensão de arquivo. Isso torna muito ruim o nome não corresponder ao URL usado para acessar o arquivo, pois o cenário é esse se você não fornecer.
Nick Albrecht
O uso da Inlinepropriedade Disposição de conteúdo permite separar a capacidade de definir o nome do arquivo do comportamento de forçar o download ou não.
Nick Albrecht
4

Acredito que esta resposta seja mais limpa (com base em 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
fonte
9
Não recomendado, o Content-Disposition é o método preferido para maior clareza e compatibilidade. stackoverflow.com/questions/10615797/…
Nick Albrecht
Obrigado por isso. Embora eu mudei o tipo MIME para application/octet-streame isso ainda fez com que o arquivo fosse baixado em vez de mostrado, e parece ser compatível.
precisa
1
Mentir sobre o tipo de conteúdo parece uma péssima ideia. Alguns navegadores dependem do tipo de conteúdo correto para sugerir aplicativos para o usuário na caixa de diálogo "salvar ou abrir".
Oskar Berggren
Se sua intenção é fazer com que o navegador sugira um aplicativo, tudo bem, mas esta pergunta é especificamente sobre forçar o download ...
Serj Sagan
@SerjSagan Acho que é mais uma maneira de burlar o comportamento dos navegadores de não exibir certos tipos de arquivos diretamente ou usar um plug-in, em vez de oferecer a opção entre salvar / abrir. Não tentei com, por exemplo, JPEG agora, então não tenho certeza do comportamento exato.
Oskar Berggren
3

FileVirtualPath -> Pesquisa \ Global Office Review.pdf

public virtual ActionResult GetFile()
{
    return File(FileVirtualPath, "application/force-download", Path.GetFileName(FileVirtualPath));
}
Bishoy Hanna
fonte
5
Isso já foi mencionado na resposta anterior e não é recomendado. Veja a seguinte pergunta para mais detalhes sobre o porquê. stackoverflow.com/questions/10615797/…
Nick Albrecht
1
Dessa forma, ele não usa recurso no servidor carregando o arquivo na memória do servidor, estou correto?
21815 Ian Jowett
1
Eu acredito que sim, pois não há necessidade de você r @CrashOverride direito
Bishoy Hanna
1

O código abaixo funcionou para mim para obter um arquivo pdf de um serviço de API e respondê-lo ao navegador - espero que ajude;

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
fonte
2
Obrigado pela resposta. Você perdeu a parte em que eu procurava uma solução que incluísse a capacidade de especificar o nome do arquivo. Você está apenas retornando bytes, para que o nome seja deduzido do URL. Eu estava usando o PDF como exemplo, mas também precisava trabalhar com vários outros tipos de arquivo no meu caso. Devo mencionar que sua solução funcionaria desde que você esteja usando o .NET Framework 4.xe MVC <= 5. Se você estiver executando o .NET Core, sua melhor aposta é usá-loMicrosoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider
Nick Albrecht
@NickAlbrecht Não usei .Net Core - o exemplo acima foi para resposta em pdf em um navegador. No entanto, se você deseja fazer o download, tente: File (fileContentBytes, System.Net.Mime.MediaTypeNames.Application.Pdf, "your file name"). Não tenho certeza se você recebeu sua resposta. por favor, deixe-me saber se isso ajuda. Obrigado pela sugestão do .Net Core. Além disso, se você achou minha resposta útil, por favor, adicione um voto.
Jonny Boy
Já resolvi o problema há muito tempo com a resposta que marquei como aceita. Eu estava apontando mais algumas advertências caso você as achasse úteis. Minha pergunta original era 2011, então isso já está datado.
Nick Albrecht
@NickAlbrecht Obrigado. Eu não sabia que você resolveu, vi que é um post muito antigo. Encontrei este fórum enquanto procurava algumas respostas relacionadas a assíncronas, sou novo nos processos assíncronos. eu era capaz de resolver meu problema, então apenas compartilhei. Do que você pelo seu tempo.
Jonny Boy
0

O método de ação precisa retornar o FileResult com um fluxo, byte [] ou caminho virtual do arquivo. Você também precisará conhecer o tipo de conteúdo do arquivo que está sendo baixado. Aqui está um método de utilitário de amostra (rápido / sujo). Exemplo de link de vídeo Como baixar arquivos usando o núcleo do 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
fonte
Vincular a algo ao qual você está afiliado (por exemplo, uma biblioteca, ferramenta, produto, tutorial ou site) sem revelar que é seu é considerado spam no Stack Overflow. Veja: O que significa "boa" autopromoção? , algumas dicas e conselhos sobre autopromoção . Qual é a definição exata de "spam" para Stack Overflow? e O que faz com que algo seja spam .
Samuel Liew
0

Se, como eu, você chegou a esse tópico através dos componentes do Razor enquanto aprende o Blazor, verá que precisa pensar um pouco mais fora da caixa para resolver esse problema. É um campo minado se (também como eu) o Blazor é sua primeira entrada no mundo do tipo MVC, pois a documentação não é tão útil para essas tarefas "domésticas".

Portanto, no momento da redação deste artigo, você não pode fazer isso usando o Blazor / Razor baunilha sem incorporar um controlador MVC para lidar com a parte de download de arquivos, um exemplo do qual é o seguinte:

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);
    }
}

Em seguida, verifique se a inicialização do aplicativo (Startup.cs) está configurada para usar corretamente o MVC e tem a seguinte linha presente (adicione-a, se não):

        services.AddMvc();

.. e, finalmente, modifique seu componente para vincular ao controlador, por exemplo (exemplo baseado em iterativo usando uma classe personalizada):

    <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>

Espero que isso ajude qualquer um que lutou (como eu!) A obter uma resposta apropriada para essa pergunta aparentemente simples nos reinos de Blazor…!

VorTechS
fonte
Embora isso possa ser útil para outras pessoas que enfrentam o mesmo problema que você, seria mais possível descobrir se você postasse sua própria pergunta com um título que indique que é exclusivo da Blazor, responda a si mesmo e adicione um comentário aqui sugerindo que alguém alcance esta pergunta com problemas relacionados ao blazor, verifique o seu link. Eu senti que estava chegando mesmo com essa pergunta original para o ASP.NET MVC e adaptando sua resposta para ser relevante para o ASP.NET Core. Blazor é um animal totalmente diferente, como você descobriu.
Nick Albrecht