Descargar archivo de cualquier tipo en Asp.Net MVC usando FileResult?

228

Me sugirieron que debería usar FileResult para permitir a los usuarios descargar archivos de mi aplicación Asp.Net MVC. Pero los únicos ejemplos de esto que puedo encontrar siempre tienen que ver con los archivos de imagen (especificando el tipo de contenido image / jpeg).

Pero, ¿y si no puedo saber el tipo de archivo? Quiero que los usuarios puedan descargar prácticamente cualquier archivo del área de archivos de mi sitio.

Había leído un método para hacer esto (ver una publicación anterior para el código), que realmente funciona bien, excepto por una cosa: el nombre del archivo que aparece en el cuadro de diálogo Guardar como se concatena desde la ruta del archivo con guiones bajos ( folder_folder_file.ext). Además, parece que la gente piensa que debería devolver un FileResult en lugar de usar esta clase personalizada que encontré BinaryContentResult.

¿Alguien sabe la forma "correcta" de hacer tal descarga en MVC?

EDITAR: obtuve la respuesta (a continuación), pero pensé que debería publicar el código de trabajo completo si alguien más está interesado:

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
fuente
12
Lo que estás haciendo es bastante peligroso. Estás permitiendo que los usuarios descarguen cualquier archivo de tu servidor al que pueda acceder el usuario ejecutor.
Paul Fleming el
1
Verdadero: eliminar la ruta del archivo y clavarlo en el cuerpo del resultado de la acción sería algo más seguro. Al menos de esa manera solo tienen acceso a una carpeta determinada.
shubniggurath
2
¿Hay alguna herramienta que le permita encontrar lagunas potencialmente peligrosas como esta?
David
Creo que es conveniente establecer el tipo de contenido como Response.ContentType = MimeMapping.GetMimeMapping(filePath);, desde stackoverflow.com/a/22231074/4573839
yu yang Jian
¿Qué estás usando del lado del cliente?
FrenkyB

Respuestas:

425

Simplemente puede especificar el tipo MIME genérico de octeto-flujo:

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
fuente
44
Ok, podría intentarlo, pero ¿qué hay en la matriz de bytes []?
Anders
3
No importa, creo que lo descubrí. Leí el nombre de archivo (ruta completa) en un FileStream y luego en una matriz de bytes, ¡y luego funcionó de maravilla! ¡Gracias!
Anders
55
Esto carga todo el archivo en la memoria solo para transmitirlo; para archivos grandes, esto es un cerdo. Una solución mucho mejor es la siguiente que no tiene que cargar primero el archivo en la memoria.
HBlackorby
13
Como esta respuesta tiene casi cinco años, sí. Si está haciendo esto para servir archivos muy grandes, no lo haga. Si es posible, use un servidor de archivos estático separado para no atar los hilos de su aplicación, o una de las muchas técnicas nuevas para servir archivos agregados a MVC desde 2010. Esto solo muestra el tipo MIME correcto para usar cuando el tipo MIME es desconocido . ReadAllBytesfue agregado años más tarde en una edición. ¿Por qué es esta mi segunda respuesta más votada? Oh bien.
Ian Henry
10
Obteniendo este error:non-invocable member "File" cannot be used like a method.
A-Sharabiani
105

El marco MVC admite esto de forma nativa. El controlador System.Web.MVC.Controller.File proporciona métodos para devolver un archivo por nombre / secuencia / matriz .

Por ejemplo, utilizando una ruta virtual al archivo, puede hacer lo siguiente.

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

Si está utilizando .NET Framework 4.5, entonces use MimeMapping.GetMimeMapping (string FileName) para obtener el tipo MIME para su archivo. Así es como lo he usado en mi acción.

return File(Path.Combine(@"c:\path", fileFromDB.FileNameOnDisk), MimeMapping.GetMimeMapping(fileFromDB.FileName), fileFromDB.FileName);
Salman Hasrat Khan
fuente
Eso hace que el mapeo de Mime sea bueno, pero ¿no es un proceso pesado descubrir cuál es el tipo de archivo en tiempo de ejecución?
Mohammed Noureldin
@MohammedNoureldin no está "imaginándolo", hay una tabla de mapeo simple basada en extensiones de archivo o algo así. El servidor lo hace para todos los archivos estáticos, no es lento.
Al Kepp
13

Phil Haack tiene un buen artículo donde creó una clase de resultado de acción de descarga de archivo personalizado. Solo necesita especificar la ruta virtual del archivo y el nombre a guardar.

Lo usé una vez y aquí está mi 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 };
        }

En mi ejemplo, estaba almacenando la ruta física de los archivos, así que utilicé este método auxiliar, que encontré en algún lugar que no recuerdo, para convertirlo en una ruta virtual

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

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

            return "~/" + physicalPath;
        }

Aquí está la clase completa tomada del artículo 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
fuente
1
Bien, sí, también vi ese artículo, pero parece hacer lo mismo que el artículo que utilicé (ver la referencia a mi publicación anterior), y él mismo dice en la parte superior de la página que la solución no debería ' ya no se necesita porque: "NUEVA ACTUALIZACIÓN: ya no se necesita este ActionResult personalizado porque ASP.NET MVC ahora incluye uno en el cuadro". Pero desafortunadamente, no dice nada más sobre cómo se va a usar.
Anders
@ManafAbuRous, si lee el código detenidamente, verá que en realidad convierte la ruta virtual en ruta física ( Server.MapPath(this.VirtualPath)), por lo que consumir esto directamente sin cambios es un poco ingenuo. Debe producir una alternativa que acepte PhysicalPathdado que eso es lo que eventualmente se requiere y es lo que está almacenando. Esto sería mucho más seguro ya que ha asumido que la ruta física y la ruta relativa serían las mismas (excluyendo la raíz). Los archivos de datos a menudo se almacenan es App_Data. Esto no es accesible como una ruta relativa.
Paul Fleming
GetVirtualPath es genial ... muy útil. ¡gracias!
Zvi Redler
6

Gracias a Ian Henry !

En caso de que necesite obtener un archivo de MS SQL Server , esta es la solución.

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

Donde AppModel es EntityFrameworkmodelo y MyFiles presenta una tabla en su base de datos. FileData está varbinary(MAX)en la tabla MyFiles .

Desarrollador
fuente
2

es simple, solo dé su ruta física en el directorio Path con el nombre del archivo

public FilePathResult GetFileFromDisk(string fileName)
{
    return File(directoryPath, "multipart/form-data", fileName);
}
DARSHAN SHINDE
fuente
¿Qué pasa con el lado del cliente, llamando a este método? Digamos si desea mostrar guardar como diálogo?
FrenkyB
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
fuente
-1

if (string.IsNullOrWhiteSpace (fileName)) devuelve Contenido ("nombre de archivo no 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
fuente
-4

GetFile debería estar cerrando el archivo (o abriéndolo dentro de un uso). Luego puede eliminar el archivo después de la conversión a bytes; la descarga se realizará en ese búfer 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;
    }

Entonces, en su método de descarga ...

        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
fuente
2
Por favor, nunca, nunca cargue archivos completos en la memoria en una producción como esta
makhdumi