82

I need to stream a file which will result in save as prompt in the browser. The issue is, the directory that the file is located is virtually mapped, so I am unable to use Server.MapPath to determine it's actual location. The directory is not in the same location (or even phyical server on the live boxes) as the website.

I'd like something like the following, but that will allow me to pass a web URL, and not a server file path.

I may have to end up building my file path from a config base path, and then append on the rest of the path, but hopefully I can do it this way instead.

var filePath = Server.MapPath(DOCUMENT_PATH);

if (!File.Exists(filePath))
    return;

var fileInfo = new System.IO.FileInfo(filePath);
Response.ContentType = "application/octet-stream";
Response.AddHeader("Content-Disposition", String.Format("attachment;filename=\"{0}\"", filePath));
Response.AddHeader("Content-Length", fileInfo.Length.ToString());
Response.WriteFile(filePath);
Response.End();
Saro Taşciyan
  • 5,210
  • 5
  • 31
  • 50
mp3duck
  • 2,633
  • 7
  • 26
  • 40
  • Can you elaborate a bit on what you mean with "virtually mapped"? A virtual IIS folder accessible by URLs? – EventHorizon Apr 08 '11 at 15:08
  • It's using a VPP path, which is a concept use by EpiServer CMS We setup the virtual path name (i.e. "/documents/"), and then specify what physical path this should map too (i.e "//servername/documents"). The system then creates this reference to the directory at runtime. You can browse to the files via the web URL without issue – mp3duck Apr 08 '11 at 15:46
  • The filename is indeed accessibile via URL. I need to use this URL to stream the file, and not the server path, as I am unable to dertmine this from the URL (using MapPath) – mp3duck Apr 08 '11 at 15:48
  • If you know the URL and the file extension opens the save as prompt in the browser (or is this one of your problems?), perhaps you could redirect the request to the file you want downloaded? Otherwise, user97970's suggestion seems to be the way to go. – EventHorizon Apr 10 '11 at 16:01

9 Answers9

113

You could use HttpWebRequest to get the file and stream it back to the client. This allows you to get the file with a url. An example of this that I found ( but can't remember where to give credit ) is

    //Create a stream for the file
    Stream stream = null;

    //This controls how many bytes to read at a time and send to the client
    int bytesToRead = 10000;

    // Buffer to read bytes in chunk size specified above
    byte[] buffer = new Byte[bytesToRead];

    // The number of bytes read
    try
    {
      //Create a WebRequest to get the file
      HttpWebRequest fileReq = (HttpWebRequest) HttpWebRequest.Create(url);

      //Create a response for this request
      HttpWebResponse fileResp = (HttpWebResponse) fileReq.GetResponse();

      if (fileReq.ContentLength > 0)
        fileResp.ContentLength = fileReq.ContentLength;

        //Get the Stream returned from the response
        stream = fileResp.GetResponseStream();

        // prepare the response to the client. resp is the client Response
        var resp = HttpContext.Current.Response;

        //Indicate the type of data being sent
        resp.ContentType = MediaTypeNames.Application.Octet;
    
        //Name the file 
        resp.AddHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
        resp.AddHeader("Content-Length", fileResp.ContentLength.ToString());
    
        int length;
        do
        {
            // Verify that the client is connected.
            if (resp.IsClientConnected)
            {
                // Read data into the buffer.
                length = stream.Read(buffer, 0, bytesToRead);

                // and write it out to the response's output stream
                resp.OutputStream.Write(buffer, 0, length);

                // Flush the data
                resp.Flush();

                //Clear the buffer
                buffer = new Byte[bytesToRead];
            }
            else
            {
                // cancel the download if client has disconnected
                length = -1;
            }
        } while (length > 0); //Repeat until no data is read
    }
    finally
    {
        if (stream != null)
        {
            //Close the input stream
            stream.Close();
        }
    }
Alex
  • 729
  • 2
  • 10
  • 24
Dallas
  • 2,217
  • 1
  • 13
  • 13
  • 2
    Flushing the response and re-creating the buffer each run is unnecessary - it will just slow down the transfer. Also, I think you should rename some of the variables (fileReq -> urlRequest, fileResp -> urlResponse). Also there is no reason to create the Stream variable, in the beginning, you should create it where you use it (near the GetResponseStream() call) – data May 11 '12 at 11:06
  • @data_smith Why don't you try what you are suggesting? It won't compile. And the purpose of the buffer is to keep memory usage down on the web server. – paparazzo Jun 09 '12 at 20:53
  • 3
    resp is not defined in this example: the code doesn't compile. – Case Jun 26 '12 at 21:47
  • 1
    As @Sharon mentioned, this code doesn't compile. What you need to do is, at the comment that reads `// prepare the response to the client. resp is the client Response`, add this bit of code: `var resp = HttpContext.Current.Response;` – Jeremy Wiggins Jul 02 '12 at 18:11
  • @Dallas Thank you for that sample, it just made my day. I'm using it while building a service that is streaming mp3 files to a Windows Phone 7 client where the AudioPlayerAgent is not able to deal with redirect 302 and then needs to get back the file stream directly. – Jonx Jul 05 '12 at 11:57
  • adding the stream to email attachment got my job done for mp3 files :)bravo – Fahad Abid Janjua Mar 22 '13 at 08:20
  • Can i use this for files with large size? for example 1 GB file? – A Programmer Feb 24 '17 at 12:40
  • Is this way good for downloading large files?i mean does not it stream file on server and then download it?i think this way first downloads file on server and then it download to users system. – A Programmer Mar 05 '18 at 10:50
  • This method doesn't download the whole file to the server. It streams the file from the source to the client. – Dallas Mar 08 '18 at 01:22
  • Exactly i use this way in WebForms and it works fine, but now i want to use same way in DotNet Core, can you provide a solution for DotNET Core? – A Programmer Mar 11 '18 at 17:12
  • @Dallas I have the very same issue with regards to file location. This solution is working for certain file types example. png, word and excel. However when I try to download for eg; .dat or .cd it returns a 404 error even though the file does exists. – Menzi Jul 24 '18 at 09:56
  • 1
    I know I'm very late to the game, but thank you so much for sharing this :) I was looking for a solution to stream a file from a remote location/URL and this worked perfectly for me. The only change I made is that I swapped `HttpWebRequest` with `SafeWebRequest` – Terry May 13 '20 at 08:08
  • System.Web.HttpContext , System.Web.HttpResponse are not supported in .net core, How can this be achieved in .net core? – user1066231 Jun 19 '20 at 18:00
  • This answer has a serious flaw: It doesn't handle redirects, or any other edge-case status codes. It also exposes the OP to low-level details that aren't necessary to get the job done. [WebClient](https://stackoverflow.com/a/35322419/712526) or [HttpClient](https://stackoverflow.com/q/45711428/712526) are much better choices. – jpaugh Oct 05 '20 at 15:22
  • I realize this is an old answer, but the question will never stop being relevant. – jpaugh Oct 05 '20 at 15:23
33

Download url to bytes and convert bytes into stream:

using (var client = new WebClient())
{
    var content = client.DownloadData(url);
    using (var stream = new MemoryStream(content))
    {
        ...
    }
}   
Berezh
  • 942
  • 9
  • 12
  • 7
    Although this seem to be simple and works fine, but you need to keep in mind that this solution loads the file into your computer Memory. Therefore chances you get an exception due to insufficient memory is high when either 1) a data is too large or 2) multiple request are being made simultaneously. – Benjamin Sep 13 '17 at 07:32
  • 3
    Benjamin, That problem can be mitigated by using [HttpClient](https://stackoverflow.com/q/45711428/712526) instead. – jpaugh Oct 05 '20 at 15:25
15

I do this quite a bit and thought I could add a simpler answer. I set it up as a simple class here, but I run this every evening to collect financial data on companies I'm following.

class WebPage
{
    public static string Get(string uri)
    {
        string results = "N/A";

        try
        {
            HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uri);
            HttpWebResponse resp = (HttpWebResponse)req.GetResponse();

            StreamReader sr = new StreamReader(resp.GetResponseStream());
            results = sr.ReadToEnd();
            sr.Close();
        }
        catch (Exception ex)
        {
            results = ex.Message;
        }
        return results;
    }
}

In this case I pass in a url and it returns the page as HTML. If you want to do something different with the stream instead you can easily change this.

You use it like this:

string page = WebPage.Get("http://finance.yahoo.com/q?s=yhoo");
CodeChops
  • 1,980
  • 1
  • 20
  • 27
  • my case is different, there is a document management system and returns docs in .tif format where no is sent in querystring. Example:[link](http://1.2.3.4/docsys/getdocuments.aspx?doc=DSF16-00260132) but this method doesn't work, any idea is appreciated. thanks – kayhan yüksel Jun 23 '16 at 05:26
  • @kayhanyüksel: I tried that link but it timed out. Sorry. – CodeChops Jun 28 '16 at 16:41
  • Just my 2 cents to the solution: Maybe you need to add some Credentials to the request via `req.Credentials = credential;` and you can directly use the `Stream` via `resp.GetResponseStream()`. – dns_nx Dec 22 '22 at 12:14
7

2 years later, I used Dallas' answer, but I had to change the HttpWebRequest to FileWebRequest since I was linking to direct files. Not sure if this is the case everywhere, but I figured I'd add it. Also, I removed

var resp = Http.Current.Resonse

and just used Http.Current.Response in place wherever resp was referenced.

Tingo
  • 472
  • 7
  • 11
  • 1
    Perfect. I stripped Dalla's answer a bit. I'm posting my version below – Ricardo Appleton Jul 18 '17 at 16:09
  • But you need to be aware of the file type you are downloading. Downloading PDF files does not work with `HttpWebRequest`, but if you are downloading a message file (.msg), it does not work with `FileWebRequest`. – dns_nx Dec 22 '22 at 11:53
7

If you are looking for a .NET Core version of @Dallas's answer, use the below.

        Stream stream = null;

        //This controls how many bytes to read at a time and send to the client
        int bytesToRead = 10000;

        // Buffer to read bytes in chunk size specified above
        byte[] buffer = new Byte[bytesToRead];

        // The number of bytes read
        try
        {
            //Create a WebRequest to get the file
            HttpWebRequest fileReq = (HttpWebRequest)HttpWebRequest.Create(@"file url");

            //Create a response for this request
            HttpWebResponse fileResp = (HttpWebResponse)fileReq.GetResponse();

            if (fileReq.ContentLength > 0)
                fileResp.ContentLength = fileReq.ContentLength;

            //Get the Stream returned from the response
            stream = fileResp.GetResponseStream();

            // prepare the response to the client. resp is the client Response
            var resp = HttpContext.Response;

            //Indicate the type of data being sent
            resp.ContentType = "application/octet-stream";

            //Name the file 
            resp.Headers.Add("Content-Disposition", "attachment; filename=test.zip");
            resp.Headers.Add("Content-Length", fileResp.ContentLength.ToString());

            int length;
            do
            {
                // Verify that the client is connected.
                if (!HttpContext.RequestAborted.IsCancellationRequested)
                {
                    // Read data into the buffer.
                    length = stream.Read(buffer, 0, bytesToRead);

                    // and write it out to the response's output stream
                    resp.Body.Write(buffer, 0, length);


                    //Clear the buffer
                    buffer = new Byte[bytesToRead];
                }
                else
                {
                    // cancel the download if client has disconnected
                    length = -1;
                }
            } while (length > 0); //Repeat until no data is read
        }
        finally
        {
            if (stream != null)
            {
                //Close the input stream
                stream.Close();
            }
        }
anonymous
  • 71
  • 1
  • 1
4

I would argue the simplest way to do so in .Net Core is:

using (MemoryStream ms = new MemoryStream())
    using (HttpClient client = new HttpClient())
    {
      client.GetStreamAsync(url).Result.CopyTo(ms);
      // use ms in what you want
    }
  }

now you have the file downloaded as stream inside ms.

YazanGhafir
  • 464
  • 3
  • 8
0

The accepted solution from Dallas was working for us if we use Load Balancer on the Citrix Netscaler (without WAF policy).

The download of the file doesn't work through the LB of the Netscaler when it is associated with WAF as the current scenario (Content-length not being correct) is a RFC violation and AppFW resets the connection, which doesn't happen when WAF policy is not associated.

So what was missing was:

Response.End();

See also: Trying to stream a PDF file with asp.net is producing a "damaged file"

0

You could try using the DirectoryEntry class with the IIS path prefix:

using(DirectoryEntry de = new DirectoryEntry("IIS://Localhost/w3svc/1/root" + DOCUMENT_PATH))
{
    filePath = de.Properties["Path"].Value;
}

if (!File.Exists(filePath))
        return;

var fileInfo = new System.IO.FileInfo(filePath);
Response.ContentType = "application/octet-stream";
Response.AddHeader("Content-Disposition", String.Format("attachment;filename=\"{0}\"", filePath));
Response.AddHeader("Content-Length", fileInfo.Length.ToString());
Response.WriteFile(filePath);
Response.End();
Andreas Paulsson
  • 7,745
  • 3
  • 25
  • 31
0

While I also disagree with the idea of overriding user preferences, here is how I did this .net6.

Add this to Startup.Configure. Essentially we just change the PDF content type and add the content-disposition on download. You can further refine to only target specific files, etc.

var extensionProvider = new FileExtensionContentTypeProvider();
extensionProvider.Mappings[".pdf"] = "application/octet-stream";

app.UseStaticFiles(new StaticFileOptions
{
    ContentTypeProvider = extensionProvider,
    OnPrepareResponse = response =>
    {
        if (response.File.Name.EndsWith(".pdf", StringComparison.InvariantCultureIgnoreCase))
        {
            response.Context.Response.Headers.Remove("content-disposition");
            response.Context.Response.Headers.Add("content-disposition","attachment; filename=\"" + response.File.Name + "\"");
        }
    }
}); 
Andrew Grothe
  • 2,562
  • 1
  • 32
  • 48