Devolver un archivo para Ver / Descargar en ASP.NET MVC

304

Tengo un problema al enviar archivos almacenados en una base de datos al usuario en ASP.NET MVC. Lo que quiero es una vista que enumere dos enlaces, uno para ver el archivo y dejar que el tipo MIME enviado al navegador determine cómo debe manejarse, y el otro para forzar una descarga.

Si elijo ver un archivo llamado SomeRandomFile.baky el navegador no tiene un programa asociado para abrir archivos de este tipo, entonces no tengo ningún problema con el valor predeterminado del comportamiento de descarga. Sin embargo, si elijo ver un archivo llamado SomeRandomFile.pdfo SomeRandomFile.jpgquiero que el archivo simplemente se abra. Pero también quiero mantener un enlace de descarga a un lado para poder forzar una solicitud de descarga independientemente del tipo de archivo. ¿Esto tiene sentido?

Lo he intentado FileStreamResulty funciona para la mayoría de los archivos, su constructor no acepta un nombre de archivo de forma predeterminada, por lo que a los archivos desconocidos se les asigna un nombre de archivo basado en la URL (que no conoce la extensión para dar según el tipo de contenido). Si forzo el nombre del archivo al especificarlo, pierdo la capacidad del navegador de abrir el archivo directamente y aparece un mensaje de descarga. ¿Alguien más ha encontrado esto?

Estos son los ejemplos de lo que he probado hasta ahora.

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

¿Alguna sugerencia?


ACTUALIZACIÓN: Esta pregunta parece tocar un acorde con mucha gente, así que pensé en publicar una actualización. La advertencia sobre la respuesta aceptada a continuación que fue agregada por Oskar con respecto a los caracteres internacionales es completamente válida, y la he golpeado varias veces debido al uso de la ContentDispositionclase. Desde entonces he actualizado mi implementación para solucionar esto. Si bien el código a continuación es de mi encarnación más reciente de este problema en una aplicación ASP.NET Core (Full Framework), también debería funcionar con cambios mínimos en una aplicación MVC anterior ya que estoy usando la System.Net.Http.Headers.ContentDispositionHeaderValueclase.

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
fuente

Respuestas:

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 ejemplo anterior no tiene en cuenta correctamente los caracteres internacionales en el nombre del archivo. Ver RFC6266 para la estandarización relevante. Creo que las versiones recientes del File()método ASP.Net MVC y la ContentDispositionHeaderValueclase explican esto adecuadamente. - Oskar 2016-02-25

Darin Dimitrov
fuente
77
Si recuerdo correctamente, no se puede citar mientras el nombre de archivo no tenga espacios (ejecuté el mío a través de HttpUtility.UrlEncode () para lograr esto).
Keith Williams
22
Nota: Si usa esto y lo configura, Inline = trueasegúrese de NO usar la sobrecarga de 3 parámetros File()que toma el nombre de archivo como el tercer parámetro. Se va a trabajar en el IE, Chrome, pero reportará un encabezado duplicado y se niegan a presentar la imagen.
Fausto
74
¿De qué tipo es var document = ...?
TTT
77
@ user1103990, es su modelo de dominio.
Darin Dimitrov
3
Con MVC 5, ya no es necesario el encabezado de disposición de contenido, ya que ya forma parte del encabezado de respuesta. Pero solo obtengo un diálogo de descarga en FF, no hay diálogo en Chrome e IE
Legends
124

Tuve problemas con la respuesta aceptada debido a la falta de sugerencias de tipo en la variable "documento": var document = ...así que estoy publicando lo que funcionó para mí como una alternativa en caso de que alguien más esté teniendo 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
fuente
44
La variable del documento era solo una clase (POCO) que representaba la información sobre el documento que deseaba devolver. Eso fue preguntado en la respuesta aceptada también. Podría provenir de un ORM, de una consulta SQL creada manualmente, del sistema de archivos (ya que el suyo extrae información) o de algún otro almacén de datos. Era irrelevante para la pregunta original de dónde provenían los bytes de documento / nombre de archivo / tipo mime, por lo que se dejó de lado para no enturbiar el código. Sin embargo, gracias por contribuir con un ejemplo utilizando solo el sistema de archivos.
Nick Albrecht
1
Esta solución no funciona correctamente cuando el nombre del archivo contiene caracteres internacionales fuera de US-ASCII.
Oskar Berggren
1
Gracias, salvó mi día :)
Dipesh
1
Una alternativa a esto AppDomain.CurrentDomain.BaseDirectoryes que System.Web.HttpContext.Current.Server.MapPath("~")esto puede funcionar mejor en un servidor real en comparación con una máquina local.
Chris Thompson
15

La respuesta de Darin Dimitrov es correcta. Solo una adición:

Response.AppendHeader("Content-Disposition", cd.ToString());puede provocar que el navegador no procese el archivo si su respuesta ya contiene un encabezado de "Disposición de contenido". En ese caso, es posible que desee utilizar:

Response.Headers.Add("Content-Disposition", cd.ToString());
León
fuente
Encuentro que el tipo de contenido también tiene influencia, para el pdfarchivo, si configuro el tipo de contenido como System.Net.Mime.MediaTypeNames.Application.Octet, forzará la descarga incluso cuando configuro Inline = true, pero si configuro como Response.ContentType = MimeMapping.GetMimeMapping(filePath), es decir application/pdf, se puede abrir correctamente en lugar de descargar
yu yang Jian
Response.Headers.Addrequieren el modo de canalización integrado de IIS. Además, incluso si el grupo de aplicaciones está configurado como integrado, arrojará una excepción. Solución. Uso Response.AddHeader. Vea el hilo SO: stackoverflow.com/questions/22313167/…
roland
12

Para ver el archivo (txt, por ejemplo):

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

Para descargar el archivo (txt, por ejemplo):

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

nota: para descargar el archivo debemos pasar el argumento fileDownloadName

Ashot Muradian
fuente
El único problema con este enfoque es que el nombre del archivo solo se proporciona si utiliza el método que fuerza una descarga. No me permite enviar el nombre de archivo correcto cuando quiero permitir que el navegador determine cómo abrir el archivo. Por lo tanto, la aplicación externa intentará usar mi URL como nombre de archivo. Que generalmente será algún tipo de identificación para el documento en la base de datos, generalmente sin una extensión de archivo. Esto hace que sea bastante pobre, el nombre no coincide con la URL utilizada para acceder al archivo, ya que el escenario es que si no proporciona.
Nick Albrecht
Usar la Inlinepropiedad en Content-Disposition me permite separar la capacidad de configurar el nombre del archivo del comportamiento de forzar la descarga o no.
Nick Albrecht
4

Creo que esta respuesta es más limpia (basada en 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
fuente
99
No recomendado, Content-Disposition es el método preferido para mayor claridad y compatibilidad. stackoverflow.com/questions/10615797/…
Nick Albrecht
Gracias por eso. Aunque cambié el tipo MIME a application/octet-streamy eso hizo que el archivo se descargara en lugar de mostrarse, y parece ser compatible.
Serj Sagan el
1
Mentir sobre el tipo de contenido parece una muy mala idea. Algunos navegadores dependen del tipo de contenido correcto para sugerir aplicaciones para el usuario en el cuadro de diálogo "guardar o abrir".
Oskar Berggren
Si su intención es que el navegador sugiera una aplicación, entonces está bien, pero esta pregunta es específicamente sobre forzar la descarga ...
Serj Sagan
@SerjSagan Creo que se trata más de eludir el comportamiento de los navegadores por defecto para ver ciertos tipos de archivos directamente o usar un complemento, en lugar de ofrecer la opción entre guardar / abrir. No probé con, por ejemplo, JPEG en este momento, así que no estoy seguro del comportamiento exacto.
Oskar Berggren
3

FileVirtualPath -> Research \ Global Office Review.pdf

public virtual ActionResult GetFile()
{
    return File(FileVirtualPath, "application/force-download", Path.GetFileName(FileVirtualPath));
}
Bishoy Hanna
fuente
55
Esto ya se mencionó en la respuesta anterior y no se recomienda. Consulte la siguiente pregunta para obtener más detalles sobre por qué. stackoverflow.com/questions/10615797/…
Nick Albrecht
1
De esta manera no utiliza recursos en el servidor al cargar el archivo en la memoria del servidor, ¿estoy en lo cierto?
Ian Jowett
1
Creo que sí, ya que no es necesario para ti bien @CrashOverride
Bishoy Hanna
1

El siguiente código funcionó para mí para obtener un archivo pdf de un servicio API y responderlo al navegador, espero que ayude;

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
fuente
2
Gracias por la respuesta. Te perdiste la parte en la que estaba buscando una solución que incluyera la capacidad de especificar el nombre del archivo. Solo está devolviendo bytes, por lo que el nombre se deducirá de la URL. Estaba usando PDF como ejemplo, pero también necesitaba que funcionara para varios otros tipos de archivos en mi caso. Debo mencionar que su solución funcionaría siempre que esté utilizando .NET Framework 4.xy MVC <= 5. Si está ejecutando contra .NET Core, su mejor opción es usarMicrosoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider
Nick Albrecht
@NickAlbrecht No utilicé .Net Core: el ejemplo anterior fue para la respuesta en PDF en un navegador. Sin embargo, si desea descargar, pruebe: File (fileContentBytes, System.Net.Mime.MediaTypeNames.Application.Pdf, "su nombre de archivo"). Sin embargo, no estoy seguro si recibió su respuesta. avíseme si esto ayuda. Sin embargo, gracias por la sugerencia de .Net Core. Además, si ha encontrado útil mi respuesta, agregue un voto.
Jonny Boy
Ya resolví el problema hace mucho tiempo con la respuesta que marqué como aceptada. Estaba más señalando algunas advertencias en caso de que las encontraras útiles. Mi pregunta original era 2011, por lo que ya está bastante anticuada.
Nick Albrecht
@NickAlbrecht Gracias. No sabía que lo resolviste, vi que es una publicación muy antigua. Encontré este foro mientras buscaba algunas respuestas asincrónicas, soy nuevo en los procesos asincrónicos. pude resolver mi problema, así que solo compartí. Gracias por tu tiempo.
Jonny Boy
0

El método de acción debe devolver FileResult con una secuencia, byte [] o ruta virtual del archivo. También necesitará saber el tipo de contenido del archivo que se está descargando. Aquí hay un ejemplo de método de utilidad (rápido / sucio). Enlace de video de muestra Cómo descargar archivos usando asp.net core

[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
fuente
Vincular a algo con lo que está afiliado (por ejemplo, una biblioteca, herramienta, producto, tutorial o sitio web) sin revelar que es suyo se considera spam en Stack Overflow. Ver: ¿Qué significa "buena" autopromoción? , algunos consejos y sugerencias sobre autopromoción , ¿Cuál es la definición exacta de "spam" para Stack Overflow? y Qué hace que algo sea spam .
Samuel Liew
0

Si, como yo, ha llegado a este tema a través de los componentes de Razor mientras aprende Blazor, entonces encontrará que necesita pensar un poco más fuera de la caja para resolver este problema. Es un poco un campo de minas si (también como yo) Blazor es tu primera incursión en el mundo tipo MVC, ya que la documentación no es tan útil para tales tareas 'serviles'.

Entonces, al momento de escribir, no puede lograr esto usando Vanilla Blazor / Razor sin incrustar un controlador MVC para manejar la parte de descarga de archivos, un ejemplo del cual es el siguiente:

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

Luego, asegúrese de que el inicio de su aplicación (Startup.cs) esté configurado para usar MVC correctamente y que tenga la siguiente línea presente (agréguelo si no):

        services.AddMvc();

.. y finalmente modifique su componente para vincularlo al controlador, por ejemplo (ejemplo basado en iteración usando una clase 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>

¡Esperemos que esto ayude a cualquiera que haya luchado (como yo) a obtener una respuesta adecuada a esta pregunta aparentemente simple en los reinos de Blazor ...!

VorTechS
fuente
Si bien esto podría ser útil para otras personas que se encuentran con el mismo problema que usted, sería más fácil de descubrir si publicara su propia pregunta con un título que indica que es exclusivo de Blazor, responda usted mismo y agregue un comentario aquí sugiriendo que cualquiera llegue esta pregunta con problemas relacionados con blazor revisa tu enlace. Sentí que estaba llegando incluso con esta pregunta original para ASP.NET MVC, y adaptando su respuesta para que sea relevante para ASP.NET Core. Blazor es una bestia totalmente diferente, como has descubierto.
Nick Albrecht