尝试使用asp.net流式传输PDF文件时出现“损坏的文件”错误。

8
在我的一个asp.net web应用程序中,我需要隐藏向用户提供的pdf文件的位置。
因此,我正在编写一个方法,从CMS系统上的位置检索其二进制内容,然后将字节数组刷新到Web用户。
不幸的是,当下载流时出现错误:"无法打开文件,因为它已损坏"(或类似于在Adobe Reader中打开文件时)。
问题1:我做错了什么? 问题2:我可以使用这种方法下载大文件吗?
    private void StreamFile(IItem documentItem)
    {
        //CMS vendor specific API
        BinaryContent itemBinaryContent = documentItem.getBinaryContent();
        //Plain old .NET
        Stream fileStream = itemBinaryContent.getContentStream();
        var len = itemBinaryContent.getContentLength();
        SendStream(fileStream, len, itemBinaryContent.getContentType());
    }

    private void SendStream(Stream stream, int contentLen, string contentType)
    {
        Response.ClearContent();
        Response.ContentType = contentType;
        Response.AppendHeader("content-Disposition", string.Format("inline;filename=file.pdf"));
        Response.AppendHeader("content-length", contentLen.ToString());
        var bytes = new byte[contentLen];
        stream.Read(bytes, 0, contentLen);
        stream.Close();
        Response.BinaryWrite(bytes);
        Response.Flush();
    }

1
为什么不使用Response.WriteFile方法? - o.k.w
1
该文件在文件系统中并不存在。 - Pablo
更明确地说:该文件存在于CMS数据库中。它具有URL地址,但我无法从文件系统中检索该文件。 - Pablo
啊,我错过了那部分。去掉 content-length 怎么样?另外,为什么需要使用 string.Format("inline;filename=file.pdf")? - o.k.w
把那个移除掉也没有任何效果,不幸的是。 - Pablo
6个回答

7
这里是我的一个方法。它返回一个附件,所以IE会弹出打开/保存对话框。我还知道文件不会超过1M,所以肯定有更简洁的方法来做这件事。
我曾遇到PDF的类似问题,我意识到我必须使用二进制流和ReadBytes。任何包含字符串的东西都会让它出错。
Stream stream = GetStream(); // Assuming you have a method that does this.
BinaryReader reader = new BinaryReader(stream);

HttpResponse response = HttpContext.Current.Response;
response.ContentType = "application/pdf";
response.AddHeader("Content-Disposition", "attachment; filename=file.pdf");
response.ClearContent();
response.OutputStream.Write(reader.ReadBytes(1000000), 0, 1000000);

// End the response to prevent further work by the page processor.
response.End();

不知道哪一行代码起作用了,但它终于能运行了 :) - Pablo
你是否在使用ReportViewer? - JoshYates1980
指定特定字节数的缺点是,即使文件不够大,所有这些字节也将被写入流中。您可以获取流的长度并在调用reader.ReadBytes时使用该数字。 - Ciaran Gallagher

7
其他答案在发送响应前将文件内容复制到内存中。如果数据已经在内存中,则会有两个副本,这对于可扩展性不是很好。以下做法可能更好:
public void SendFile(Stream inputStream, long contentLength, string mimeType, string fileName)
{
    string clength = contentLength.ToString(CultureInfo.InvariantCulture);
    HttpResponse response = HttpContext.Current.Response;
    response.ContentType = mimeType;
    response.AddHeader("Content-Disposition", "attachment; filename=" + fileName);
    if (contentLength != -1) response.AddHeader("Content-Length", clength);
    response.ClearContent();
    inputStream.CopyTo(response.OutputStream);
    response.OutputStream.Flush();
    response.End();
}

由于流是字节的集合,因此无需使用BinaryReader。只要输入流以文件结尾,您就可以在要发送到Web浏览器的流上使用CopyTo()方法。所有内容都将写入目标流,而不会制作任何中间数据副本。

如果您需要仅从流中复制一定数量的字节,则可以创建一个扩展方法,添加几个更多的CopyTo()重载:

public static class Extensions
{
    public static void CopyTo(this Stream inStream, Stream outStream, long length)
    {
        CopyTo(inStream, outStream, length, 4096);
    }

    public static void CopyTo(this Stream inStream, Stream outStream, long length, int blockSize)
    {
        byte[] buffer = new byte[blockSize];
        long currentPosition = 0;

        while (true)
        {
            int read = inStream.Read(buffer, 0, blockSize);
            if (read == 0) break;
            long cPosition = currentPosition + read;
            if (cPosition > length) read = read - Convert.ToInt32(cPosition - length);
            outStream.Write(buffer, 0, read);
            currentPosition += read;
            if (currentPosition >= length) break;
        }
    }
}

您可以像这样使用它:
inputStream.CopyTo(response.OutputStream, contentLength);

这个方法适用于任何输入流,但是一个快速的例子就是从文件系统中读取一个文件:
string filename = @"C:\dirs.txt";
using (FileStream fs = File.Open(filename, FileMode.Open))
{
    SendFile(fs, fs.Length, "application/octet-stream", filename);
}

如前所述,请确保您的MIME类型与内容匹配。


1
这段代码对我很有帮助:
                Response.Clear();
                Response.ClearContent();
                Response.ClearHeaders();
                Response.ContentType = mimeType;
                Response.AddHeader("Content-Disposition", "attachment");
                Response.WriteFile(filePath);
                Response.Flush();

0

当您使用代码时,您正在将PDF中的字节直接复制到响应中。所有特殊的ASCII码都需要进行编码以通过HTTP传递。使用流功能时,流会被编码,因此您不必担心它。

    var bytes = new byte[contentLen];
    stream.Read(bytes, 0, contentLen);
    stream.Close();
    Response.BinaryWrite(bytes);

0
这段代码包括从文件路径读取文件并提取文件名的代码:
private void DownloadFile(string path)
{
    using (var file = System.IO.File.Open(path, FileMode.Open))
    {
        var buffer = new byte[file.Length];
        file.Read(buffer, 0, (int)file.Length);

        var fileName = GetFileName(path);
        WriteFileToOutputStream(fileName, buffer);
    }
}

private string GetFileName(string path)
{
    var fileNameSplit = path.Split('\\');
    var fileNameSplitLength = fileNameSplit.Length;
    var fileName = fileNameSplit[fileNameSplitLength - 1].Split('.')[0];
    return fileName;
}

private void WriteFileToOutputStream(string fileName, byte[] buffer)
{
    Response.Clear();
    Response.ContentType = "application/pdf";
    Response.AddHeader("Content-Disposition",
        $"attachment; filename\"{fileName}.pdf");
    Response.OutputStream.Write(buffer, 0, buffer.Length);
    Response.OutputStream.Close();
    Response.Flush();
    Response.End();
}

它将整个文件读入一个字节数组中,并保持文件处于打开状态。 - CodeCaster
你会推荐什么改变呢?干杯 - Ciaran Gallagher
我是否还需要调用Response.OutputStream.Close()方法? - Ciaran Gallagher
我需要在文件的开头使用 using 语句吗? - Ciaran Gallagher

0

我在当前的2.0网站上已经实现了类似的功能。我记得曾经很费劲才让它正常工作,但已经过了一段时间,我不记得当时为什么费力。

然而,我的实现与你的有一些不同之处。希望这些可以帮助你解决问题。

  • 在ClearContent调用后,我会调用ClearHeaders();
  • 我没有指定长度。
  • 我没有在Disposition中指定inline。
  • 我知道所有人都说不要这样做,但我使用了Response.Flush();,并紧接着使用了Response.Close();

还有一件事,检查一下contentType值,确保它对于PDFs是正确的(("Application/pdf")。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接