Sunday, June 8, 2014

How to stream a FileResult from one web server to another with ASP.NET MVC

MVC has a lot of great built in tooling, including the ability to stream very large file results straight from disk without having to load the whole file stream into memory.

What about the scenario where you want to stream a large file from one web server to another?

For example, I have an ASP.NET MVC application that needs to expose a download for a file hosted on another server, but I can not just redirect my users directly to the other URL. For that, we need to create a custom ActionResult type!

WebRequestFileResult

Here is a simple of example of what your controller might look like:

public class FileController : Controller
{
    public ActionResult LocalFile()
    {
        return new FilePathResult(@"c:\files\otherfile.zip", "application/zip");
    }
 
    public ActionResult RemoteFile()
    {
        return new WebRequestFileResult("http://otherserver/otherfile.zip");
    }
}

Here is the implementation for WebRequestFileResult:

public class WebRequestFileResult : FileResult
{
    private const int BufferSize = 32768; // 32 KB
    private const string DispositionHeader = "Content-Disposition";
    private const string LengthHeader = "Content-Length";
 
    private readonly string _url;
 
    public WebRequestFileResult(string url)
        : base("application/octet-stream")
    {
        if (String.IsNullOrWhiteSpace(url))
            throw new ArgumentNullException("url", "Url is required");
 
        _url = url;
    }
 
    protected override void WriteFile(HttpResponseBase localResponse)
    {
        var webRequest = WebRequest.Create(_url);
 
        using (var remoteResponse = webRequest.GetResponse())
        using (var remoteStream = remoteResponse.GetResponseStream())
        {
            if (remoteStream == null)
                throw new NullReferenceException(
                    "Request returned null stream: " + _url);
 
            var dispositionKey = remoteResponse.Headers.AllKeys.FirstOrDefault(
                k => k.Equals(
                    DispositionHeader, 
                    StringComparison.InvariantCultureIgnoreCase));
 
            if (!String.IsNullOrWhiteSpace(dispositionKey))
                localResponse.AddHeader(
                    DispositionHeader, 
                    remoteResponse.Headers[DispositionHeader]);
 
            var contentLenthString = remoteResponse.ContentLength
                .ToString(CultureInfo.InvariantCulture);
                
            localResponse.ContentType = remoteResponse.ContentType;
            localResponse.AddHeader(LengthHeader, contentLenthString);
 
            var buffer = new byte[BufferSize];
            var loopCount = 0;
 
            while (true)
            {
                if (!localResponse.IsClientConnected)
                    break;
 
                var read = remoteStream.Read(buffer, 0, BufferSize);
 
                if (read <= 0)
                    break;
 
                localResponse.OutputStream.Write(buffer, 0, read);
 
                // Flush after 1 MB; note that this
                // will prevent server side caching.
                loopCount++;
                if (loopCount % 32 == 0)
                    localResponse.Flush();
            }
        }
    }
}

Enjoy,
Tom

6 comments:

  1. You should use Stream.Copy method instead of while(true) loop.

    ReplyDelete
    Replies
    1. When using Stream.CopyTo the response stream would not always auto flush, and the server could built up a lot of memory pressure.

      Delete
    2. How can filestreamresult support resume download from url , Tom ?

      Delete
  2. You completed certain reliable points there. I did a search on the subject and found nearly all persons will agree with your blog. gostream

    ReplyDelete
  3. We are very excited to say that in Q2 2020 (April 1 to June 30) we saw more community involvement than ever before. Many pull requests were submitted that spanned from bug fixes for our low-level assembly to higher-level modules such as the AppKit framework. Thanks to everyone for your contributions and we hope for this level of engagement to continue.

    GOM Player Plus crack

    idm crack

    Inpixio Photo Studio Ultimate Crack

    Malwarebytes Anti Exploit Premium Crack

    IObit Driver Booster Pro Crack


    ReplyDelete

Real Time Web Analytics