ส่งคืนไฟล์เพื่อดู / ดาวน์โหลดใน 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};

ข้อเสนอแนะใด ๆ


UPDATE: คำถามนี้ดูเหมือนจะตอบโต้กับผู้คนจำนวนมากดังนั้นฉันคิดว่าฉันโพสต์การอัปเดต คำเตือนเกี่ยวกับคำตอบที่ได้รับการยอมรับด้านล่างซึ่งเพิ่มโดย Oskar เกี่ยวกับตัวละครต่างประเทศนั้นถูกต้องสมบูรณ์และฉันใช้งานไม่กี่ครั้งเนื่องจากใช้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ระดับบัญชีเหมาะสมสำหรับสิ่งนี้ - Oskar 2016-02-25


7
ถ้าฉันจำได้อย่างถูกต้องมันสามารถยกเลิกการอ้างอิงได้ตราบใดที่ชื่อไฟล์ไม่มีช่องว่าง (ฉันวิ่งผ่าน HttpUtility.UrlEncode () เพื่อให้ได้สิ่งนี้)
Keith Williams

22
หมายเหตุ: หากคุณใช้สิ่งนี้และตั้งค่าInline = trueอย่าใช้เกิน 3-param File()ที่ใช้ชื่อไฟล์เป็นพารามิเตอร์ที่ 3 มันจะทำงานใน IE แต่ Chrome จะรายงานส่วนหัวที่ซ้ำกันและปฏิเสธที่จะนำเสนอภาพ
เฟาสต์

74
เอกสาร var คือประเภทใด ...
TTT

7
@ user1103990 เป็นรุ่นของโดเมนของคุณ
ดารินทินดิมิทรอฟ

3
การใช้ MVC 5 ไม่จำเป็นต้องมีส่วนหัวการจัดการเนื้อหาอีกต่อไปเนื่องจากเป็นส่วนหนึ่งของส่วนหัวการตอบกลับแล้ว แต่ฉันจะได้รับไดอะล็อกดาวน์โหลดใน FF เท่านั้นไม่มีไดอะล็อกใน chrome และ IE
ตำนาน

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

4
ตัวแปรเอกสารเป็นเพียงคลาส (POCO) ที่แสดงข้อมูลเกี่ยวกับเอกสารที่คุณต้องการส่งคืน ที่ถูกถามถึงคำตอบที่ได้รับการยอมรับเช่นกัน อาจมาจาก ORM จากแบบสอบถาม SQL ที่สร้างขึ้นด้วยตนเองจากระบบไฟล์ (ตามที่คุณดึงข้อมูลจาก) หรือที่เก็บข้อมูลอื่น ๆ มันไม่เกี่ยวข้องกับคำถามต้นฉบับที่ประเภทเอกสารไบต์ / ชื่อไฟล์ / mime ของคุณมาจากดังนั้นจึงถูกปล่อยออกมาเพื่อไม่ให้โค้ดยุ่งเหยิง ขอขอบคุณที่บริจาคตัวอย่างโดยใช้ระบบไฟล์
Nick Albrecht

1
วิธีการแก้ปัญหานี้ทำงานไม่ถูกต้องเมื่อชื่อไฟล์มีอักขระนานาชาติอยู่นอก US-ASCII
Oskar Berggren

1
ขอขอบคุณที่บันทึกไว้วันของฉัน :)
Dipesh

1
อีกทางเลือกหนึ่งAppDomain.CurrentDomain.BaseDirectoryคือSystem.Web.HttpContext.Current.Server.MapPath("~")สิ่งนี้อาจทำงานได้ดีขึ้นบนเซิร์ฟเวอร์จริงเมื่อเทียบกับเครื่องท้องถิ่น
Chris Thompson

15

คำตอบของดารินดิมิทรอฟถูกต้อง เพียงแค่เพิ่ม:

Response.AppendHeader("Content-Disposition", cd.ToString());อาจทำให้เบราว์เซอร์ล้มเหลวในการแสดงไฟล์ถ้าคำตอบของคุณมีส่วนหัว "การจัดการเนื้อหา" อยู่แล้ว ในกรณีนั้นคุณอาจต้องการใช้:

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 ของฉันเป็นชื่อไฟล์ ซึ่งโดยปกติจะเป็น ID บางประเภทสำหรับเอกสารในฐานข้อมูลโดยทั่วไปจะไม่มีนามสกุลไฟล์ สิ่งนี้ทำให้คนยากจนชื่อไม่ตรงกับ URL ที่ใช้ในการเข้าถึงไฟล์สำหรับสถานการณ์สมมติว่าถ้าคุณไม่ได้ระบุ
Nick Albrecht

การใช้Inlineคุณสมบัติบน Content-Disposition ช่วยให้ฉันสามารถแยกความสามารถในการตั้งชื่อไฟล์จากพฤติกรรมการบังคับให้ดาวน์โหลดหรือไม่
Nick Albrecht

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
ไม่แนะนำการกำจัดเนื้อหาเป็นวิธีที่ต้องการเพื่อความชัดเจนและความเข้ากันได้ stackoverflow.com/questions/10615797/…
Nick Albrecht

ขอบคุณสำหรับสิ่งนั้น แม้ว่าฉันจะเปลี่ยนประเภท MIME เป็นapplication/octet-streamและยังทำให้ไฟล์ดาวน์โหลดมากกว่าที่แสดงและดูเหมือนว่าจะเข้ากันได้
Serj Sagan

1
การโกหกเกี่ยวกับประเภทเนื้อหาดูเหมือนเป็นความคิดที่ไม่ดีจริงๆ เบราว์เซอร์บางตัวขึ้นอยู่กับประเภทเนื้อหาที่ถูกต้องเพื่อแนะนำแอปพลิเคชันสำหรับผู้ใช้ในกล่องโต้ตอบ "บันทึกหรือเปิด"
Oskar Berggren

หากเจตนาของคุณคือให้เบราว์เซอร์แนะนำแอปแล้วก็ดี แต่คำถามนี้เกี่ยวกับการบังคับให้ดาวน์โหลดโดยเฉพาะ ...
Serj Sagan

@SerjSagan ฉันคิดว่ามันเกี่ยวกับการหลีกเลี่ยงพฤติกรรมเบราว์เซอร์ของการเริ่มต้นการดูไฟล์บางประเภทโดยตรงหรือใช้ปลั๊กอินแทนที่จะเสนอตัวเลือกระหว่างการบันทึก / เปิด ไม่ได้ลองเช่น JPEG ในขณะนี้ดังนั้นจึงไม่แน่ใจในพฤติกรรมที่แน่นอน
Oskar Berggren

3

FileVirtualPath -> Research \ Global Office Review.pdf

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

5
สิ่งนี้ถูกกล่าวถึงแล้วในคำตอบก่อนหน้าและไม่แนะนำ ดูคำถามต่อไปนี้สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับสาเหตุ stackoverflow.com/questions/10615797/…
Nick Albrecht

1
วิธีนี้จะไม่ใช้ทรัพยากรบนเซิร์ฟเวอร์โดยการโหลดไฟล์ลงในหน่วยความจำบนเซิร์ฟเวอร์ฉันถูกต้องหรือไม่
Ian Jowett

1
ฉันเชื่อว่าอย่างที่ไม่จำเป็นสำหรับคุณที่ถูกต้อง @CrashOverride
Bishoy Hanna

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

@NickAlbrecht ฉันไม่ได้ใช้. Net Core - ตัวอย่างข้างต้นใช้สำหรับการตอบกลับ pdf ในเบราว์เซอร์ อย่างไรก็ตามหากคุณต้องการดาวน์โหลดลอง: ไฟล์ (fileContentBytes, System.Net.Mime.MediaTypeNames.Application.Pdf, "ชื่อไฟล์ของคุณ") ฉันไม่แน่ใจว่าถ้าคุณได้รับคำตอบ โปรดแจ้งให้เราทราบหากนี่เป็นประโยชน์สำหรับคำแนะนำ. Net Core นอกจากนี้หากคุณพบว่าคำตอบของฉันมีประโยชน์โปรดเพิ่มคะแนน
จอนนี่บอย

ฉันแก้ไขปัญหานี้มานานแล้วด้วยคำตอบที่ฉันทำเครื่องหมายว่ายอมรับแล้ว ฉันชี้ให้เห็นมากกว่าสองประการในกรณีที่คุณพบว่ามีประโยชน์ คำถามเดิมของฉันคือ 2011 ดังนั้นตอนนี้ค่อนข้างเก่า
Nick Albrecht

@NickAlbrecht ขอบคุณ ฉันไม่ทราบว่าคุณแก้ไขมันฉันไม่เห็นมันเป็นโพสต์เก่ามาก ฉันพบฟอรัมนี้ในขณะที่ค้นหาคำตอบที่เกี่ยวข้องกับ async ฉันยังใหม่กับกระบวนการของ async ฉันสามารถแก้ปัญหาของฉันได้ดังนั้นจึงต้องแบ่งปัน กว่าคุณสำหรับเวลาของคุณ
Jonny Boy

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"},
            ...
        };
    }
}

การลิงก์ไปยังสิ่งที่คุณเป็นพันธมิตร (เช่นห้องสมุดเครื่องมือผลิตภัณฑ์บทแนะนำหรือเว็บไซต์) โดยไม่เปิดเผยว่าเป็นของคุณและถือว่าเป็นสแปมใน Stack Overflow ดู: การส่งเสริมตัวเองของ "ดี" หมายถึงอะไร? , เคล็ดลับและคำแนะนำเกี่ยวกับการส่งเสริมตนเอง , ความหมายที่แท้จริงของ "สแปม" สำหรับกองมากเกินคืออะไร? และสิ่งที่ทำให้บางสิ่งบางอย่างสแปม
ซามูเอล Liew

0

ถ้าอย่างฉันคุณได้มาที่หัวข้อนี้ด้วยองค์ประกอบของมีดโกนในขณะที่คุณกำลังเรียนรู้ 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 ... !


แม้ว่าสิ่งนี้จะเป็นประโยชน์สำหรับผู้อื่นที่พบปัญหาเดียวกันกับคุณ แต่จะสามารถค้นพบได้มากขึ้นหากคุณโพสต์คำถามของคุณเองด้วยชื่อที่ระบุว่าเป็นเอกลักษณ์ของ Blazor ตอบคำถามด้วยตัวคุณเองและเพิ่มความคิดเห็นที่นี่ คำถามนี้มีปัญหาเกี่ยวกับ blazor ลองดูที่ลิงค์ ฉันรู้สึกว่าฉันถึงแม้จะมีคำถามเดิมนี้สำหรับ ASP.NET MVC และปรับคำตอบให้ตรงกับ ASP.NET Core Blazor นั้นเป็นสัตว์ร้ายที่แตกต่างอย่างสิ้นเชิงอย่างที่คุณค้นพบ
Nick Albrecht
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.