Baixar arquivo de qualquer tipo no Asp.Net MVC usando o FileResult?

228

Sugeri que eu usasse o FileResult para permitir que os usuários baixassem arquivos do meu aplicativo Asp.Net MVC. Mas os únicos exemplos disso que encontro sempre têm a ver com arquivos de imagem (especificando o tipo de conteúdo image / jpeg).

Mas e se eu não souber o tipo de arquivo? Quero que os usuários possam baixar praticamente qualquer arquivo da área de arquivos do meu site.

Eu tinha lido um método para fazer isso (consulte uma postagem anterior para o código), que realmente funciona bem, exceto por uma coisa: o nome do arquivo que aparece na caixa de diálogo Salvar como é concatenado no caminho do arquivo com sublinhados ( folder_folder_file.ext). Além disso, parece que as pessoas pensam que eu deveria retornar um FileResult em vez de usar essa classe personalizada que eu encontrei BinaryContentResult.

Alguém conhece a maneira "correta" de fazer esse download no MVC?

EDIT: recebi a resposta (abaixo), mas pensei em postar o código de trabalho completo se outra pessoa estiver interessada:

public ActionResult Download(string filePath, string fileName)
{
    string fullName = Path.Combine(GetBaseDir(), filePath, fileName);

    byte[] fileBytes = GetFile(fullName);
    return File(
        fileBytes, System.Net.Mime.MediaTypeNames.Application.Octet, fileName);
}

byte[] GetFile(string s)
{
    System.IO.FileStream fs = System.IO.File.OpenRead(s);
    byte[] data = new byte[fs.Length];
    int br = fs.Read(data, 0, data.Length);
    if (br != fs.Length)
        throw new System.IO.IOException(s);
    return data;
}
Anders
fonte
12
O que você está fazendo é bastante perigoso. Você está praticamente permitindo que os usuários baixem qualquer arquivo do seu servidor que o usuário em execução possa acessar.
Paul Fleming
1
Verdadeiro - remover o caminho do arquivo e fixá-lo no corpo do resultado da ação seria um pouco mais seguro. Pelo menos dessa maneira, eles só têm acesso a uma determinada pasta.
shubniggurath
2
Existem ferramentas que permitem encontrar brechas potencialmente perigosas como esta?
David
Acho que é conveniente para definir o tipo de conteúdo como Response.ContentType = MimeMapping.GetMimeMapping(filePath);, a partir stackoverflow.com/a/22231074/4573839
Yang Yu Jian
O que você está usando no lado do cliente?
precisa saber é o seguinte

Respostas:

425

Você pode apenas especificar o tipo MIME genérico de octet-stream:

public FileResult Download()
{
    byte[] fileBytes = System.IO.File.ReadAllBytes(@"c:\folder\myfile.ext");
    string fileName = "myfile.ext";
    return File(fileBytes, System.Net.Mime.MediaTypeNames.Application.Octet, fileName);
}
Ian Henry
fonte
4
Ok, eu poderia tentar isso, mas o que se passa na matriz de bytes []?
Anders
3
Não importa, acho que descobri. Eu li o nome do arquivo (caminho completo) em um FileStream e depois em uma matriz de bytes, e funcionou como um encanto! Obrigado!
Anders
5
Isso carrega o arquivo inteiro na memória apenas para transmiti-lo; para arquivos grandes, isso é um porco. Uma solução muito melhor é a abaixo, que não precisa carregar o arquivo na memória primeiro.
HBlackorby
13
Como essa resposta tem quase cinco anos, sim. Se você estiver fazendo isso para servir arquivos muito grandes, não. Se possível, use um servidor de arquivos estático separado para não amarrar os encadeamentos de aplicativos ou uma das muitas novas técnicas para servir arquivos adicionados ao MVC desde 2010. Isso mostra apenas o tipo MIME correto a ser usado quando o tipo MIME é desconhecido . ReadAllBytesfoi adicionado anos depois em uma edição. Por que essa é a minha segunda resposta mais votada? Ah bem.
Ian Henry
10
Obtendo este erro:non-invocable member "File" cannot be used like a method.
A-Sharabiani 02/02
105

A estrutura MVC suporta isso nativamente. O controlador System.Web.MVC.Controller.File fornece métodos para retornar um arquivo por nome / fluxo / matriz .

Por exemplo, usando um caminho virtual para o arquivo, você pode fazer o seguinte.

return File(virtualFilePath, System.Net.Mime.MediaTypeNames.Application.Octet,  Path.GetFileName(virtualFilePath));
Jonathan
fonte
36

Se você estiver usando o .NET Framework 4.5, use o MimeMapping.GetMimeMapping (string FileName) para obter o Tipo MIME do seu arquivo. É assim que eu o usei na minha ação.

return File(Path.Combine(@"c:\path", fileFromDB.FileNameOnDisk), MimeMapping.GetMimeMapping(fileFromDB.FileName), fileFromDB.FileName);
Salman Hasrat Khan
fonte
Esse mapeamento do Mime é bom, mas não é um processo de heave descobrir qual é o tipo de arquivo em tempo de execução?
Mohammed Noureldin
@MohammedNoureldin não está "entendendo", existe uma tabela de mapeamento simples baseada em extensões de arquivo ou algo parecido. O servidor faz isso para todos os arquivos estáticos, não é lento.
Al Kepp
13

Phil Haack tem um bom artigo em que criou uma classe de resultado de ação de download de arquivo personalizado. Você só precisa especificar o caminho virtual do arquivo e o nome a ser salvo como.

Eu usei uma vez e aqui está o meu código.

        [AcceptVerbs(HttpVerbs.Get)]
        public ActionResult Download(int fileID)
        {
            Data.LinqToSql.File file = _fileService.GetByID(fileID);

            return new DownloadResult { VirtualPath = GetVirtualPath(file.Path),
                                        FileDownloadName = file.Name };
        }

No meu exemplo, eu estava armazenando o caminho físico dos arquivos, então usei esse método auxiliar - que encontrei em algum lugar que não me lembro - para convertê-lo em um caminho virtual

        private string GetVirtualPath(string physicalPath)
        {
            string rootpath = Server.MapPath("~/");

            physicalPath = physicalPath.Replace(rootpath, "");
            physicalPath = physicalPath.Replace("\\", "/");

            return "~/" + physicalPath;
        }

Aqui está a aula completa tirada do artigo de Phill Haack

public class DownloadResult : ActionResult {

    public DownloadResult() {}

    public DownloadResult(string virtualPath) {
        this.VirtualPath = virtualPath;
    }

    public string VirtualPath {
        get;
        set;
    }

    public string FileDownloadName {
        get;
        set;
    }

    public override void ExecuteResult(ControllerContext context) {
        if (!String.IsNullOrEmpty(FileDownloadName)) {
            context.HttpContext.Response.AddHeader("content-disposition", 
            "attachment; filename=" + this.FileDownloadName)
        }

        string filePath = context.HttpContext.Server.MapPath(this.VirtualPath);
        context.HttpContext.Response.TransmitFile(filePath);
    }
}
Manaf Abu.Rous
fonte
1
Certo, sim, eu vi esse artigo também, mas parece fazer o mesmo que o artigo que usei (consulte a referência ao meu post anterior), e ele diz a si mesmo no topo da página que a solução alternativa não deve ser ' não é mais necessário porque: "NOVA ATUALIZAÇÃO: não há mais necessidade deste ActionResult personalizado, porque o ASP.NET MVC agora inclui um na caixa". Infelizmente, ele não diz mais nada sobre como isso deve ser usado.
Anders
@ManafAbuRous, se você ler o código de perto, verá que, na verdade, ele converte o caminho virtual em caminho físico ( Server.MapPath(this.VirtualPath)), portanto consumi-lo diretamente sem alterações é um pouco ingênuo. Você deve produzir uma alternativa que aceite, PhysicalPathdado que é o que é eventualmente necessário e é o que você está armazenando. Isso seria muito mais seguro, pois você assumiu que o caminho físico e o caminho relativo seriam os mesmos (excluindo a raiz). Os arquivos de dados geralmente são armazenados como App_Data. Isso não está acessível como um caminho relativo.
Paul Fleming
GetVirtualPath é ótimo .... muito útil. obrigado!
Zvi Redler 15/06
6

Graças a Ian Henry !

Caso você precise obter um arquivo do MS SQL Server, aqui está a solução.

public FileResult DownloadDocument(string id)
        {
            if (!string.IsNullOrEmpty(id))
            {
                try
                {
                    var fileId = Guid.Parse(id);

                    var myFile = AppModel.MyFiles.SingleOrDefault(x => x.Id == fileId);

                    if (myFile != null)
                    {
                        byte[] fileBytes = myFile.FileData;
                        return File(fileBytes, System.Net.Mime.MediaTypeNames.Application.Octet, myFile.FileName);
                    }
                }
                catch
                {
                }
            }

            return null;
        }

Onde AppModel é EntityFrameworkmodelo e MyFiles apresenta a tabela em seu banco de dados. FileData está varbinary(MAX)na tabela MyFiles .

Desenvolvedor
fonte
2

é simples, basta fornecer seu caminho físico no directoryPath com o nome do arquivo

public FilePathResult GetFileFromDisk(string fileName)
{
    return File(directoryPath, "multipart/form-data", fileName);
}
DARSHAN SHINDE
fonte
E o lado do cliente, chamando esse método? Vamos dizer se você deseja mostrar salvar como caixa de diálogo?
precisa saber é o seguinte
0
   public ActionResult Download()
        {
            var document = //Obtain document from database context
    var cd = new System.Net.Mime.ContentDisposition
    {
        FileName = document.FileName,
        Inline = false,
    };
            Response.AppendHeader("Content-Disposition", cd.ToString());
            return File(document.Data, document.ContentType);
        }
hossein zakizadeh
fonte
-1

if (string.IsNullOrWhiteSpace (fileName)) retorna Content ("nome do arquivo não presente");

        var path = Path.Combine(your path, your filename);

        var stream = new FileStream(path, FileMode.Open);

        return File(stream, System.Net.Mime.MediaTypeNames.Application.Octet, fileName);
Caio Augusto
fonte
-4

GetFile deve fechar o arquivo (ou abri-lo dentro de um uso). Em seguida, você pode excluir o arquivo após a conversão em bytes - o download será feito no buffer de bytes.

    byte[] GetFile(string s)
    {
        byte[] data;
        using (System.IO.FileStream fs = System.IO.File.OpenRead(s))
        {
            data = new byte[fs.Length];
            int br = fs.Read(data, 0, data.Length);
            if (br != fs.Length)
                throw new System.IO.IOException(s);
        }
        return data;
    }

Então, no seu método de download ...

        byte[] fileBytes = GetFile(file);
        // delete the file after conversion to bytes
        System.IO.File.Delete(file);
        // have the file download dialog only display the base name of the file            return File(fileBytes, System.Net.Mime.MediaTypeNames.Application.Octet, Path.GetFileName(file));
CDichter
fonte
2
Por favor, não nunca, nunca carregar arquivos inteiros na memória em produção como esta
makhdumi