Resumable downloads in Silverlight Out-Of-Browser applications

The task facing us is simple.  In fact, it has been done before.  Many download management tools already have the ability to resume downloading a file if, for some reason, the process was interrupted.  There is no built-in facility for resuming an interrupted download within a Silverlight applications.  Silverlight does provide some tools for working around this limitation, but only while running under elevated privileges in an out-of-browser application.

A little background

The HTTP/1.1 specification defines an Accept-Ranges response header.  This header is not required by servers and therefore is not guaranteed to be there whenever a file needs to be downloaded.  Servers that do support it will typically send back a header that includes the following header fields:

Accept-Ranges: bytes | none
Content-Length: xxxx

The Accept-Ranges field will equal “bytes” if the server supports byte range on GET commands.   Additionally, the Content-Length field will equal the total size of the file being downloaded.

Once a server has been determined to support byte ranges, HTTP GET requests can include an additional header field to retrieve a block of data.  The size of the block is defined by the application by sending start and end indices.  The following header field request the first 1024 bytes from the server.

Ranges: bytes=0-1023

Silverlight Implementation

With that background, the process for creating a resumable downloader in Silverlight OOB is rather straight-forward.

  1. Check to see if the file has not finished downloading
  2. Assuming it’s not finished, request next chunk of data and append to the existing partial file.
  3. Continue requesting chunks and appending until finished.

Several implementations are possible: for example, a temporary file can be used to hold the partial file and its existence lets the implementation know that the download is not finished.  An alternative method could be to compare the length of the file on disk with the length returned from the HEAD Http request. 

The purpose of the following code snippets are to convey the concepts behind creating a resumable downloader rather than provide a complete implementation.  A full-featured resumable downloader may be posted at future data, time permitting.  But there should be enough here to get you going.

Creating an Http/1.1 HEAD request

In order to get started we need to know two things: whether the server supports the Range header and the size of the file we want to download.  The HEAD request will enable us, in essence, to query the capabilities of the server and determine the size of the file in one round-trip.  Using the HttpWebRequest class is necessary to make the HEAD request.  We use the asynchronous BeginGetResponse() method to initiate the communication with the server.

WebRequest.RegisterPrefix("http://", WebRequestCreator.ClientHttp);
WebRequest.RegisterPrefix("https://", WebRequestCreator.ClientHttp);

HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(downloadUri);
webRequest.Method = "HEAD";

webRequest.BeginGetResponse(OnHttpCheckHeadersResponse, webRequest);

By setting the request method to “HEAD”, only the relevant header information will be returned and the actual download will not start.  In the callback, the header can be processed and the download can be started based on the results. Below is a sample of what the callback may look like.

private void OnHttpCheckHeadersResponse(IAsyncResult result)
{
    HttpWebRequest request = result.AsyncState as HttpWebRequest;
    if (request == null) throw new Exception("There was a problem with the web request.");

    try
    {
        var response = (HttpWebResponse)request.EndGetResponse(result);
        var acceptsRange = response.Headers["Accept-Ranges"];

        if (acceptsRange.Equals("bytes", StringComparison.CurrentCultureIgnoreCase))
        {
            // Accepts partial downloads.
            string sizeString = response.Headers["Content-Length"];
            if (long.TryParse(sizeString, out _length))
            {
                ResumableDownload();
            }
        }
        else
        {
            // Download the whole thing at once and hope for the best.
            NonResumableDownload();
        }
    }
    catch (Exception e)
    {
        // Possibly a 404 error.
        throw e;
    }
}

Of course, the actual implementation may differ.  For example, header information could be stored in a separate structure, firing events or using messages to signal completion of the header parsing are all viable options.

The remainder of the implementation requires downloading the actual file one piece at a time.  Assuming that the downloader may be in a state where a portion of the file was downloaded earlier, a check is needed to determine where to begin.  If we’re using a temporary file, the downloader can first determine if the file exists, and if it does, determine its current size.  That code may look something like the following, where DownloadBlockSize is a constant used to define the size of the block to download:

string destinationFile = CreateDestinationFileFromUri(downloadUri);
FileInfo tempFile = new FileInfo(destinationFile + ".tmp");

if (!tempFile.Exists)
{
    StartBytesIndex = 0;
    EndBytesIndex = DownloadBlockSize;
}
else
{
    StartBytesIndex = tempFile.Length;
    EndBytesIndex = StartBytesIndex + DownloadBlockSize;
}

At this point, the downloader now has a starting and ending index to request a portion of the file from the server.  The final piece of the puzzle simply sets up a loop to continually download chunks of data from the server until there is no more data.  Once again, an HttpWebRequest is used to create a GET request with the Range header property set to the appropriate block of data by simply using the StartByteIndex and EndByteIndex discovered when resuming the download.

public void ResumableDownload()
{
    WebRequest.RegisterPrefix("http://", WebRequestCreator.ClientHttp);
    WebRequest.RegisterPrefix("https://", WebRequestCreator.ClientHttp);

    HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(downloadUri);
    webRequest.Method = "HEAD";

    webRequest.Headers["Range"] = string.Format("bytes={0}-{1}", 
        StartBytesIndex, 
        (EndBytesIndex <= Length) ? EndBytesIndex.ToString() : "");

    webRequest.BeginGetResponse(OnHttpGetRangeResponse, webRequest);
}

Like the HEAD request implementation, the majority of the GET request resides in the asynchronous callback.  In the code snippet below, the object, partialFileStream is a previously opened writeable stream to the temporary file.  Since the downloader will attempt to download as much as possible, it should not close and reopen the file with each range received from the server.  The Flush() method will insure that the block of data is written to the temporary file.

private void OnHttpGetRangeResponse(IAsyncResult result)
{
    HttpWebRequest request = result.AsyncState as HttpWebRequest;
    if (request == null) throw new Exception("There was a problem with the web request.");

    var response = request.EndGetResponse(result);

    using (Stream responseStream = response.GetResponseStream())
    {
        CopyStream(responseStream, partialFileStream);
        responseStream.Close();
    }

    partialFileStream.Flush();
    OnDownloadRangeComplete();

    StartBytesIndex += DownloadBlockSize + 1;
    EndBytesIndex = StartBytesIndex + DownloadBlockSize;

    if (RemainingBytes > 0)
        ResumableDownload();
    else
        FinishDownload();
}

To finish the implementation, all that needs to be done is close the open file handle and possibly rename any temporary files.

private void FinishDownload()
{
    partialFileStream.Close();

    RenameTemporaryFile();
    OnDownloadComplete();
}

 

Conclusion

The capability for writing a resumable downloader resides within the Http/1.1 specification.  Implementing the specification in Silverlight requires that Silverlight runs out-of-browser and has elevated privileges.  Silverlight applications not running with these caveats will not be able to query the file system for the existing of the partially downloaded file or arbitrarily write to files.  Silverlight requires explicit permission from the user to begin writing to the file system (this would eliminate using temporary files).  Theoretically, there may be an implementation that will allow for resumable downloads in vanilla Silverlight, but the usability will probably be questionable.  Of course, this may change in a future version of Silverlight.

As always, constructive feedback is welcome.

We deliver solutions that accelerate the value of Azure.

Ready to experience the full power of Microsoft Azure?

Start Today

Blog Home

Stay Connected

Upcoming Events

All Events