Возврат файла для просмотра / загрузки в ASP.NET MVC

304

Я столкнулся с проблемой отправки файлов, хранящихся в базе данных, обратно пользователю в ASP.NET MVC. То, что я хочу, - это представление, в котором перечислены две ссылки: одна для просмотра файла, и пусть mimetype, отправляемый в браузер, определяет, как он должен обрабатываться, а другая для принудительной загрузки.

Если я выбрал просмотр файла с именем, SomeRandomFile.bakа в браузере нет соответствующей программы для открытия файлов этого типа, у меня не возникнет проблем с его настройками по умолчанию при загрузке. Однако, если я решу просмотреть файл с именем SomeRandomFile.pdfили SomeRandomFile.jpgя хочу, чтобы файл просто открылся. Но я также хочу оставить ссылку для скачивания в стороне, чтобы можно было принудительно вызвать приглашение к загрузке независимо от типа файла. Имеет ли это смысл?

Я пробовал, FileStreamResultи он работает для большинства файлов, его конструктор не принимает имя файла по умолчанию, поэтому неизвестным файлам присваивается имя файла на основе URL-адреса (который не знает расширение для предоставления на основе типа содержимого). Если я задаю имя файла, указав его, я теряю способность браузера открывать файл напрямую и получаю приглашение на загрузку. кто-нибудь еще сталкивался с этим?

Это примеры того, что я пробовал до сих пор.

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

Какие-либо предложения?


ОБНОВЛЕНИЕ: Эти вопросы, кажется, вызывают отклик у многих людей, поэтому я решил опубликовать обновление. Предупреждение о принятом ответе ниже, которое было добавлено Оскаром в отношении международных символов, является полностью действительным, и я ударил его несколько раз из-за использования ContentDispositionкласса. С тех пор я обновил свою реализацию, чтобы исправить это. Хотя приведенный ниже код взят из моего последнего воплощения этой проблемы в приложении ASP.NET Core (Full Framework), он должен работать с минимальными изменениями и в более старом приложении MVC, так как я использую этот System.Net.Http.Headers.ContentDispositionHeaderValueкласс.

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
}
Ник Альбрехт
источник

Ответы:

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

ПРИМЕЧАНИЕ. В приведенном выше примере кода неправильно учитываются международные символы в имени файла. См. RFC6266 для соответствующей стандартизации. Я полагаю, что последние версии метода ASP.Net MVC File()и ContentDispositionHeaderValueкласс правильно объясняют это. - Оскар 2016-02-25

Дарин димитров
источник
7
Если я правильно помню, это может быть без кавычек, пока имя файла не имеет пробелов (я выполнил мой через HttpUtility.UrlEncode () для достижения этого).
Кит Уильямс
22
Примечание: Если вы используете это и установите Inline = true, убедитесь, что НЕ используете 3-х параметрическую перегрузку, File()которая принимает имя файла в качестве 3-го параметра. Он будет работать в IE, но Chrome сообщит о дублирующем заголовке и откажется представить изображение.
Фауст
74
К какому типу относится var document = ...?
TTT
7
@ user1103990, это модель вашего домена.
Дарин Димитров
3
Используя MVC 5, больше нет необходимости в заголовке размещения контента, поскольку он уже является частью заголовка ответа. Но я получаю только диалог загрузки в FF, без диалога в Chrome и IE
Legends
124

У меня возникли проблемы с принятым ответом из-за отсутствия намеков на тип переменной «document»: var document = ...поэтому я публикую то, что сработало для меня в качестве альтернативы, на случай, если у кого-то еще возникнут проблемы.

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
источник
4
Переменная документа - это просто класс (POCO), представляющий информацию о документе, который вы хотите вернуть. Это было спрошено на принятом ответе также. Он может поступать из ORM, из SQL-запроса, созданного вручную, из файловой системы (когда ваша информация извлекает информацию) или из другого хранилища данных. Это не имело значения для исходного вопроса, откуда пришел ваш документ bytes / filename / mime type, поэтому он был оставлен без ошибок. Спасибо за предоставленный пример, использующий только файловую систему.
Ник Альбрехт
1
Это решение не работает правильно, когда имя файла содержит международные символы за пределами US-ASCII.
Оскар Берггрен
1
Спасибо, спас мой день :)
Дипеш
1
Альтернативой AppDomain.CurrentDomain.BaseDirectoryявляется то System.Web.HttpContext.Current.Server.MapPath("~"), что это может работать лучше на реальном сервере по сравнению с локальной машиной.
Крис Томпсон
15

Дарин Димитров ответ правильный. Просто дополнение:

Response.AppendHeader("Content-Disposition", cd.ToString());может привести к сбою в отображении файла в браузере, если ваш ответ уже содержит заголовок «Content-Disposition». В этом случае вы можете использовать:

Response.Headers.Add("Content-Disposition", cd.ToString());
Лео
источник
Я обнаружил, что тип содержимого также оказывает влияние на pdfфайл, если я установлю тип содержимого как System.Net.Mime.MediaTypeNames.Application.Octet, он будет принудительно загружаться, даже когда я его установлю Inline = true, но если я установлю как Response.ContentType = MimeMapping.GetMimeMapping(filePath), то есть application/pdfон может открываться правильно, а не загружаться
yu Ян Цзянь
Response.Headers.Addтребуется IIS интегрированный режим конвейера. Кроме того, даже если пул приложений установлен как интегрированный, он выдаст исключение. Решение. Использование Response.AddHeader. См SO нить: stackoverflow.com/questions/22313167/...
RoLand
12

Для просмотра файла (например, txt):

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

Чтобы загрузить файл (например, txt):

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

примечание: чтобы скачать файл, мы должны передать аргумент fileDownloadName

Ашот Мурадян
источник
Единственная проблема этого подхода заключается в том, что имя файла указывается только в том случае, если вы используете метод, который вызывает загрузку. Это не позволяет мне отправлять правильное имя файла, когда я хочу позволить браузеру определить, как открыть файл. Поэтому внешнее приложение будет пытаться использовать мой URL в качестве имени файла. Обычно это будет своего рода идентификатор документа в базе данных, обычно без расширения файла. Это делает для довольно плохого имя не совпадает с URL-адресом, используемым для доступа к файлу, для сценария, который, если вы не предоставите
Ник Альбрехт
Использование Inlineсвойства в Content-Disposition позволяет мне отделить возможность установки имени файла от поведения принудительного скачивания или нет.
Ник Альбрехт
4

Я считаю, что этот ответ чище, (на основе 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));
    }
Серж Саган
источник
9
Не рекомендуется, Content-Disposition является предпочтительным методом для ясности и совместимости. stackoverflow.com/questions/10615797/…
Ник Альбрехт
Спасибо за это. Хотя я изменил тип MIME на, application/octet-streamи это все равно стало причиной загрузки файла, а не его показа, и, похоже, он совместим.
Серж Саган
1
Ложь о типе контента звучит как очень плохая идея. Некоторые браузеры зависят от правильного типа контента, чтобы предлагать приложения для пользователя в диалоговом окне «сохранить или открыть».
Оскар Берггрен
Если ваше намерение состоит в том, чтобы браузер предлагал приложение, тогда это нормально, но этот вопрос конкретно касается принудительной загрузки ...
Серж Саган
@SerjSagan Я думаю, что это больше об обходе поведения браузеров по умолчанию для просмотра файлов определенных типов напрямую или с помощью плагина, вместо того, чтобы предлагать выбор между сохранением / открытием. Не пытался, например, с JPEG прямо сейчас, поэтому не уверен в точном поведении.
Оскар Берггрен
3

FileVirtualPath -> Исследования \ Global Office Review.pdf

public virtual ActionResult GetFile()
{
    return File(FileVirtualPath, "application/force-download", Path.GetFileName(FileVirtualPath));
}
Бишой ханна
источник
5
Это уже упоминалось в предыдущем ответе и не рекомендуется. Смотрите следующий вопрос для более подробной информации о том, почему. stackoverflow.com/questions/10615797/…
Ник Альбрехт
1
Таким образом, он не использует ресурсы на сервере, загружая файл в память на сервере, я прав?
Ян Джоветт
1
Я так считаю, что нет необходимости для вас не г правильного @CrashOverride
Bishoy Ханна
1

Приведенный ниже код помог мне получить pdf-файл из службы API и отправить его в браузер - надеюсь, это поможет;

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);
    }
Джонни Бой
источник
2
Спасибо за ответ. Вы пропустили ту часть, где я искал решение, которое включало в себя возможность указать имя файла. Вы просто возвращаете байты, поэтому имя будет выведено из URL. Я использовал PDF в качестве примера, но мне нужно было, чтобы он работал и для нескольких других типов файлов в моем случае. Я должен отметить, что ваше решение будет работать до тех пор, пока вы используете .NET Framework 4.x и MVC <= 5. Если вы работаете с .NET Core, вам лучше всего использоватьMicrosoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider
Ник Альбрехт
@NickAlbrecht. Я не использовал .Net Core - приведенный выше пример был для pdf-ответа в браузере. Однако, если вы хотите скачать, попробуйте: File (fileContentBytes, System.Net.Mime.MediaTypeNames.Application.Pdf, "ваше имя файла"). Хотя я не уверен, что ты получил ответ. пожалуйста, дайте мне знать, если это поможет. Спасибо за предложение .Net Core, хотя. Также, если вы нашли мой ответ полезным, пожалуйста, добавьте голос.
Джонни Бой
Я уже решил проблему с ответом, который пометил как принятый. Я больше указал на несколько предостережений на случай, если вы сочтете их полезными. Мой первоначальный вопрос был в 2011 году, так что он уже устарел.
Ник Альбрехт
@NickAlbrecht Спасибо. Я не знал, что ты решил это, я видел, что это очень старый пост. Я нашел этот форум во время поиска некоторых асинхронных ответов, я новичок в асинхронных процессах. я смог решить мою проблему, поэтому просто поделился. Чем ты уделил время.
Джонни Бой
0

Метод действия должен возвращать FileResult либо с потоком, байтом [], либо с виртуальным путем файла. Вам также необходимо знать тип содержимого загружаемого файла. Вот пример (быстрый / грязный) служебный метод. Пример ссылки на видео Как скачать файлы с использованием ядра 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
источник
Связывание с чем-то, с чем вы связаны (например, с библиотекой, инструментом, продуктом, учебным пособием или веб-сайтом) без разглашения своего, считается спамом в переполнении стека. Видите: Что означает «Хорошее» самореклама? , Некоторые советы и рекомендации о саморекламе , Что такое точное определение понятия «спам» для переполнения стека? и что делает что-то спамом .
Самуэль Лью
0

Если, как и я, вы зашли в эту тему с помощью компонентов Razor, изучая Blazor, то вам придется подумать немного больше, чтобы решить эту проблему. Это немного минное поле, если (как и я) Blazor - ваш первый опыт в мире MVC-типа, поскольку документация не так полезна для таких «черных» задач.

Таким образом, на момент написания статьи вы не можете достичь этого, используя vanilla Blazor / Razor, не встраивая контроллер MVC для обработки части загрузки файла, пример которой приведен ниже:

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

Затем убедитесь, что запуск вашего приложения (Startup.cs) настроен для правильного использования MVC и содержит следующую строку (добавьте ее, если нет):

        services.AddMvc();

.. и, наконец, измените ваш компонент, например, для связи с контроллером (итеративный пример с использованием пользовательского класса):

    <tbody>
        @foreach (var attachment in yourAttachments)
        {
        <tr>
            <td><a href="api/FileHandling?attachmentId=@attachment.TaskAttachmentId" 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>

Надеюсь, это поможет любому, кто изо всех сил (как я!), Получить соответствующий ответ на этот, казалось бы, простой вопрос в сферах Blazor…!

VorTechS
источник
Хотя это может быть полезно для других, сталкивающихся с той же проблемой, что и вы, для них было бы более заметным, если бы вы опубликовали свой собственный вопрос с заголовком, который указывает, что он уникален для Blazor, ответьте на него самостоятельно и добавите комментарий, предлагающий любому, кто достигнет этот вопрос с проблемами, касающимися блазора, проверьте вашу ссылку. Я чувствовал, что достиг даже этого оригинального вопроса, касающегося ASP.NET MVC, и адаптировал ответ, чтобы он соответствовал ASP.NET Core. Блазор - совершенно другой зверь, как вы уже обнаружили.
Ник Альбрехт