Zwracanie pliku do Wyświetl / Pobierz w ASP.NET MVC


304

Mam problem z wysyłaniem plików przechowywanych w bazie danych z powrotem do użytkownika w ASP.NET MVC. To, czego chcę, to widok z listą dwóch linków, z których jeden służy do wyświetlania pliku i pozwala typowi mimetalnemu wysłanemu do przeglądarki określić, jak powinien być obsługiwany, a drugi do wymuszenia pobrania.

Jeśli zdecyduję się wyświetlić plik o nazwie, SomeRandomFile.baka przeglądarka nie ma powiązanego programu do otwierania plików tego typu, nie mam problemu z domyślnym zachowaniem pobierania. Jeśli jednak zdecyduję się wyświetlić plik o nazwie SomeRandomFile.pdflub SomeRandomFile.jpgchcę go po prostu otworzyć. Ale chcę też zachować odsyłacz pobierania z boku, aby móc wymusić monit o pobranie niezależnie od typu pliku. Czy to ma sens?

Próbowałem FileStreamResulti działa dla większości plików, jego konstruktor domyślnie nie akceptuje nazwy pliku, więc nieznanym plikom przypisuje się nazwę pliku na podstawie adresu URL (który nie zna rozszerzenia, które można podać na podstawie typu zawartości). Jeśli wymuszę nazwę pliku, określając go, tracę możliwość bezpośredniego otwarcia pliku przez przeglądarkę i pojawia się monit o pobranie. Czy ktoś jeszcze tego doświadczył?

To są przykłady tego, czego do tej pory próbowałem.

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

Jakieś sugestie?


AKTUALIZACJA: To pytanie wydaje się uderzać w wielu ludzi, więc pomyślałem, że opublikuję aktualizację. Ostrzeżenie dotyczące zaakceptowanej odpowiedzi poniżej, które zostało dodane przez Oskara w odniesieniu do znaków międzynarodowych, jest całkowicie ważne i trafiłem go kilka razy z powodu korzystania z ContentDispositionklasy. Od tego czasu zaktualizowałem moją implementację, aby to naprawić. Chociaż poniższy kod pochodzi z mojego ostatniego wcielenia tego problemu w aplikacji ASP.NET Core (Full Framework), powinien on również działać przy minimalnych zmianach w starszej aplikacji MVC, ponieważ używam tej System.Net.Http.Headers.ContentDispositionHeaderValueklasy.

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
}

Odpowiedzi:


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

UWAGA: Powyższy przykładowy kod nie uwzględnia poprawnie międzynarodowych znaków w nazwie pliku. Odpowiednia normalizacja znajduje się w RFC6266. Uważam, że najnowsze wersje metody ASP.Net MVC File()i ContentDispositionHeaderValueklasa poprawnie to uwzględniają. - Oskar 25.02.2016


7
Jeśli dobrze pamiętam, można go cofnąć, o ile w nazwie pliku nie ma spacji (aby to osiągnąć, uruchomiłem mój przez HttpUtility.UrlEncode ()).
Keith Williams

22
Uwaga: Jeśli użyjesz tego i ustawisz, Inline = trueupewnij się, że NIE użyjesz przeciążenia 3-param, File()który przyjmuje nazwę pliku jako 3. parametr. To będzie pracować w IE, ale Chrome zgłosi duplikat nagłówek i odmawiają przedstawienia obrazu.
Faust

74
Jakiego typu jest var document = ...?
TTT

7
@ user1103990, to Twój model domeny.
Darin Dimitrov

3
Korzystając z MVC 5, nie ma już potrzeby umieszczania nagłówka zawartości, ponieważ jest on już częścią nagłówka odpowiedzi. Ale dostaję tylko okno dialogowe pobierania w FF, brak okna dialogowego w chrome i IE
Legends

124

Miałem problem z zaakceptowaną odpowiedzią z powodu braku podpowiedzi typu w zmiennej „document”: var document = ...Więc zamieszczam to, co działało dla mnie jako alternatywę na wypadek, gdyby ktokolwiek miał problemy.

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

4
Zmienna dokumentu była tylko klasą (POCO) reprezentującą informacje o dokumencie, który chcesz zwrócić. Zostało to również zadane w odpowiedzi zaakceptowanej. Może pochodzić z ORM, z ręcznie zbudowanego zapytania SQL, z systemu plików (ponieważ twoje pobiera informacje) lub z innego magazynu danych. Nie miało to znaczenia dla pierwotnego pytania, skąd pochodzi twój bajt / nazwa pliku / mime dokumentu, więc nie pomylono kodu. Dziękujemy za przesłanie przykładu z wykorzystaniem tylko systemu plików.
Nick Albrecht,

1
To rozwiązanie nie działa poprawnie, gdy nazwa pliku zawiera znaki międzynarodowe poza US-ASCII.
Oskar Berggren,

1
Dzięki, Uratowałem mój dzień :)
Dipesh

1
Alternatywą AppDomain.CurrentDomain.BaseDirectoryjest to System.Web.HttpContext.Current.Server.MapPath("~"), że może działać lepiej na prawdziwym serwerze w porównaniu do komputera lokalnego.
Chris Thompson

15

Odpowiedź Darina Dimitrowa jest poprawna. Tylko dodatek:

Response.AppendHeader("Content-Disposition", cd.ToString());może spowodować, że przeglądarka nie wyświetli pliku, jeśli Twoja odpowiedź zawiera już nagłówek „Content-Disposition”. W takim przypadku możesz użyć:

Response.Headers.Add("Content-Disposition", cd.ToString());

Uważam, że typ zawartości ma również wpływ na pdfplik, jeśli ustawię typ zawartości jako System.Net.Mime.MediaTypeNames.Application.Octet, wymusi pobranie nawet wtedy, gdy ustawię Inline = true, ale jeśli ustawię jako Response.ContentType = MimeMapping.GetMimeMapping(filePath), to znaczy application/pdf, że można go poprawnie otworzyć, a nie pobrać
yu Yang Jian

Response.Headers.Addwymagają zintegrowanego trybu potokowego IIS. Dodatkowo, nawet jeśli pula aplikacji jest ustawiona jako zintegrowana, wygeneruje wyjątek. Rozwiązanie. Zastosowanie Response.AddHeader. Zobacz temat SO: stackoverflow.com/questions/22313167/…
roland

12

Aby wyświetlić plik (na przykład txt):

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

Aby pobrać plik (na przykład txt):

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

Uwaga: aby pobrać plik, należy przekazać argument fileDownloadName


Jedynym problemem związanym z tym podejściem jest to, że nazwa pliku jest podawana tylko w przypadku użycia metody wymuszającej pobranie. Nie pozwala mi wysłać prawidłowej nazwy pliku, gdy chcę, aby przeglądarka określiła sposób otwarcia pliku. Dlatego zewnętrzna aplikacja spróbuje użyć mojego adresu URL jako nazwy pliku. Który będzie zwykle jakimś identyfikatorem dokumentu w bazie danych, zwykle bez rozszerzenia pliku. To sprawia, że ​​dość słaba nazwa nie pasuje do adresu URL użytego do uzyskania dostępu do pliku, w scenariuszu jest to, że jeśli nie podasz.
Nick Albrecht

Użycie Inlinewłaściwości w Content-Disposition pozwala mi oddzielić możliwość ustawiania nazwy pliku od zachowania wymuszającego pobieranie lub nie.
Nick Albrecht

4

Uważam, że ta odpowiedź jest bardziej przejrzysta (na podstawie 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
Niezalecane, Content-Disposition jest preferowaną metodą dla przejrzystości i kompatybilności. stackoverflow.com/questions/10615797/…
Nick Albrecht,

Dziękuję za to. Chociaż zmieniłem typ MIME na application/octet-streami to wciąż spowodowało, że plik został pobrany, a nie pokazany, i wydaje się być zgodny.
Serj Sagan

1
Kłamstwo na temat rodzaju treści wydaje się naprawdę złym pomysłem. Niektóre przeglądarki zależą od poprawnego typu zawartości, aby sugerować aplikacje dla użytkownika w oknie dialogowym „zapisz lub otwórz”.
Oskar Berggren,

Jeśli masz zamiar poprosić przeglądarkę o zasugerowanie aplikacji, to w porządku, ale to pytanie dotyczy w szczególności wymuszenia pobrania ...
Serj Sagan

@SerjSagan Myślę, że chodzi bardziej o obejście zachowania przeglądarki, polegającego na domyślnym oglądaniu określonych typów plików bezpośrednio lub korzystaniu z wtyczki, zamiast oferowania wyboru pomiędzy zapisz / otwórz. Nie próbowałem teraz z np. JPEG, więc nie jestem pewien co do dokładnego zachowania.
Oskar Berggren,

3

FileVirtualPath -> Research \ Global Office Review.pdf

public virtual ActionResult GetFile()
{
    return File(FileVirtualPath, "application/force-download", Path.GetFileName(FileVirtualPath));
}

5
Zostało to już wspomniane w poprzedniej odpowiedzi i nie jest zalecane. Zobacz następujące pytanie, aby uzyskać więcej informacji na temat przyczyny. stackoverflow.com/questions/10615797/…
Nick Albrecht

1
W ten sposób nie wykorzystuje zasobów na serwerze, ładując plik do pamięci na serwerze, czy mam rację?
Ian Jowett,

1
Uważam, że nie ma potrzeby, abyś miał rację @CrashOverride
Bishoy Hanna

1

Poniższy kod działał dla mnie w celu pobrania pliku pdf z usługi API i wysłania go do przeglądarki - mam nadzieję, że to pomoże;

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
Dziękuję za odpowiedź. Brakowało Ci części, w której szukałem rozwiązania, które obejmowało możliwość określenia nazwy pliku. Zwracasz tylko bajty, więc nazwa zostanie wyprowadzona z adresu URL. Użyłem PDF jako przykładu, ale potrzebowałem go do pracy z wieloma innymi typami plików również w moim przypadku. Powinienem wspomnieć, że twoje rozwiązanie będzie działać, dopóki używasz .NET Framework 4.x i MVC <= 5. Jeśli korzystasz z .NET Core, najlepiej jest użyćMicrosoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider
Nick Albrecht

@NickAlbrecht Nie korzystałem z .Net Core - powyższy przykład dotyczył odpowiedzi pdf w przeglądarce. Jeśli jednak chcesz pobrać, spróbuj: Plik (fileContentBytes, System.Net.Mime.MediaTypeNames.Application.Pdf, „Twoja nazwa pliku”). Nie jestem jednak pewien, czy dostałeś swoją odpowiedź. proszę dać mi znać, jeśli to pomoże. Dzięki za sugestię .Net Core. Również jeśli uznasz moją odpowiedź za przydatną, dodaj głos.
Jonny Boy,

Już dawno rozwiązałem problem z odpowiedzią, którą oznaczyłem jako zaakceptowaną. Bardziej wskazałem kilka ostrzeżeń na wypadek, gdyby okazały się przydatne. Moje pierwotne pytanie brzmiało 2011 r., Więc jest już dość przestarzałe.
Nick Albrecht

@NickAlbrecht Dziękujemy. Nie wiedziałem, że to rozwiązałeś, widziałem, że to bardzo stary post. Znalazłem to forum, szukając odpowiedzi związanych z asynchronizacją. Jestem nowy w procesach asynchronicznych. udało mi się rozwiązać mój problem, dlatego właśnie udostępniono. Dziękuję za poświęcony czas.
Jonny Boy

0

Metoda akcji musi zwrócić FileResult ze strumieniem, bajtem [] lub wirtualną ścieżką pliku. Będziesz także musiał znać typ zawartości pobieranego pliku. Oto przykładowa (szybka / brudna) metoda narzędzia. Przykładowy link wideo Jak pobierać pliki za pomocą 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"},
            ...
        };
    }
}

Łączenie z czymś, z czym jesteś powiązany (np. Biblioteka, narzędzie, produkt, samouczek lub strona internetowa) bez ujawnienia, że ​​to twoje, jest uważane za spam w przepełnieniu stosu. Zobacz: Co oznacza „dobra” autopromocja? , Kilka wskazówek i porad na temat autopromocji , Jaka jest dokładna definicja „spam” na przepełnienie stosu? i Co powoduje, że coś jest spamem .
Samuel Liew

0

Jeśli, podobnie jak ja, przyszedłeś do tego tematu za pomocą komponentów Razor, ucząc się Blazora, musisz pomyśleć nieco więcej, aby rozwiązać ten problem. To trochę pole minowe, jeśli (tak jak ja) Blazor jest twoim pierwszym wcieleniem do świata typu MVC, ponieważ dokumentacja nie jest tak pomocna dla takich „służebnych” zadań.

W chwili pisania tego tekstu nie można tego osiągnąć za pomocą wanilii Blazor / Razor bez osadzenia kontrolera MVC w celu obsługi części pobierania plików, której przykład jest następujący:

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

Następnie upewnij się, że uruchomienie aplikacji (Startup.cs) jest skonfigurowane do prawidłowego korzystania z MVC i ma następujący wiersz (dodaj go, jeśli nie):

        services.AddMvc();

.. a następnie w końcu zmodyfikuj komponent, aby połączyć się ze sterownikiem, na przykład (przykład oparty na iteracji z wykorzystaniem klasy niestandardowej):

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

Mam nadzieję, że pomoże to każdemu, kto miał problemy (jak ja!), Aby uzyskać odpowiednią odpowiedź na to z pozoru proste pytanie w dziedzinie Blazora…!


Chociaż może to być przydatne dla innych osób, które napotkały ten sam problem, co Ty, byłoby dla nich łatwiej odkryć, jeśli opublikujesz własne pytanie z tytułem wskazującym, że jest on unikalny dla Blazora, sam odpowiedz na nie i dodaj tutaj komentarz sugerujący, że ktoś sięgnie to pytanie z problemami dotyczącymi blazora sprawdź link. Wydawało mi się, że sięgam nawet po to oryginalne pytanie do ASP.NET MVC i dostosowuję jego odpowiedź, aby była odpowiednia dla ASP.NET Core. Blazor to zupełnie inna bestia, jak odkryłeś.
Nick Albrecht,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.