views:

501

answers:

4

Hi all,

In one of my asp.net web applications I need to hide the location of a pdf file being served to the users.

Thus, I am writing a method that retrieves its binary content from its location on a CMS system and then flushes a byte array to the web user.

I'm getting, unfortunately, an error when downloading the stream: "Could not open the file because it is damadged" (or something similar to that, when opening the file in adobe reader).

Question 1: what am I doing wrong? Question 2: can I download large files using this approach?

    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();
    }
A: 

I've got something similar working on a current 2.0 web site. And I remember struggling to get it to work though it's been a while so I don't remember the struggles.

There are, though, a few differences between what I have what you have. Hopefully these will help you solve the problem.

  • After the ClearContent call, I call ClearHeaders();
  • I'm not specifying a length.
  • I'm not specifing inline on the Disposition
  • I know everyone says not to do it, but I have a Response.Flush(); followed by Response.Close();

And, one other thing, check your contentType value to make sure it is correct for PDFs (("Application/pdf").

Jeff Siver
+2  A: 

Here is a method I use. This passes back an attachment, so IE produces an Open/Save dialog. I also happen to know that the files will not be larger than 1M, so I'm sure there's a cleaner way to do this.

I had a similar problem with PDFs, and I realized that I absolutely had to use Binary streams and ReadBytes. Anything with strings messed it up.

    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();
JJO
Don't know which bit of code made it work, but it's finally working :)Thanks!
pablo
A: 

This Snippet did it for me:

                Response.Clear();
                Response.ClearContent();
                Response.ClearHeaders();
                Response.ContentType = mimeType;
                Response.AddHeader("Content-Disposition", "attachment");
                Response.WriteFile(filePath);
                Response.Flush();
Igor Zelaya
A: 

The other answers copy the file contents into memory before sending the response. If the data is already in memory, then you will have two copies, which is not very good for scalability. This may work better instead:

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

Since a Stream is a collection of bytes, there is no need to use a BinaryReader. And as long as the input stream ends at the end of the file, then you can just use the CopyTo() method on the stream you want to send to the web browser. All the contents will be written to the target stream, without making any intermediate copies of the data.

If you need to only copy a certain number of bytes from the stream, you can create an extension method that adds a couple more CopyTo() overloads:

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

You could then use it like so:

inputStream.CopyTo(response.OutputStream, contentLength);

This would work with any input stream, but a quick example would be reading a file from the file system:

string filename = @"C:\dirs.txt";
using (FileStream fs = File.Open(filename, FileMode.Open))
{
    SendFile(fs, fs.Length, "application/octet-stream", filename);
}

As mentioned before, make sure your MIME type is correct for the content.

Extremeswank