It’s a known fact that popular web servers like IIS and Apache have included support for HTTP Range Units for a long time. What this does is create an opportunity for a client (a web browser, for example) to ask for a specific portion of a resource, not the resource in its entirety.
This is used more often than you think. The obvious example is when you’re downloading a large file and the user accidentally closes the browser, or the connection fails for any reason and the download is aborted. Then, when the user tries to download the same file again, the browser can support “resuming”. This means that the browser asks for the remainder of the file and if the server supports the Range Units it will only send that portion that is asked.
Another example of its use is in HTML5 video. Only the first few bytes of the video are downloaded so the player can start playing what it has while it sends more HTTP requests to get the rest of the content. In fact, popular players like the iPhone and iPad don't support downloadable video unless the server supports Range Units. When serving static content, the web server can do this for us, as it knows the file that is to be delivered. But when the content comes from a database, or in our case an ADAM system, it’s up to the developer. In this article, I’ll explain how.
Scott Mitchell wrote a nice article on the subject a while ago, and though I took most of code he posted there, I had to make it fit for ADAM. In the process I will explain a bit on how it works, but the article I just mentioned is as thorough as it gets on the subject matter.
Basically, the client issues a simple GET request for the demanded resource. The server responds with the usual headers, Content-Type, Content-Length, ETag and Last-Modified, and if it supports Range Units it will include a header named Accept-Ranges with a value of ‘bytes’.
This allows the client to support resuming, as it is now entitled to send requests for that resource that include the headers “Range” and “If-Range”, indicating that the server should only send this specific range of bytes if the value of If-Range matches the ETag of the resource (as you know, in order to detect resources that have been invalidated)
To implement this feature, I added a new generic handler to my ASP.NET solution and made it a derived class from Scott’s RangeRequestHandlerBase class in which he has done all the hard work. All I have to do now is override a couple of methods, namely GetRequestedFileInfo, GetRequestedFileMimeType and GetRequestedFileEntityTag.
The method GetRequestedFileInfo is used by the base class to determine which file needs to be returned. We always get a path from ADAM when asking for specific files, previews or additional files of an ADAM record so we can use that path to construct one, so this one is pretty straightforward to implement. In our version, we want to download a specific file from a specific record, so we can search for the necessary parameters in our query string.
As you can see, we also fetch the MIME type of the file, as well as calculating the ETag to return which is in our case the Id of the FileVersion object. If someone decides to add a newer version later, we’ll automatically get the whole file again. Lastly, I added a header to force a download dialog box and set the filename for the file to be downloaded. This obviously should be omitted if this code is to be used to stream video content.
| C# |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
public class RangedHandler : RangeRequestHandlerBase
{
private string _ContentType;
private string _EntityTag;
public override FileInfo GetRequestedFileInfo(HttpContext context)
{
Guid recordId = new Guid(context.Request["r"]);
Guid fileId = new Guid(context.Request["f"]);
Application application = AdamContext.Current.Applications.DefaultApplication;
Record record = new Record(application);
record.Load(recordId);
File file = record.Files[fileId];
FileVersion latestVersion = file.Versions.Latest;
FileType fileType = new FileType(application);
_ContentType = fileType.TryLoad(latestVersion.FileTypeId) == TryLoadResult.Success ? fileType.MimeType : null;
_EntityTag = latestVersion.Id.ToString("N");
this.AddHeader(context.Response, "Content-disposition", "attachment; filename=" + latestVersion.FileName);
return new FileInfo(latestVersion.Path);
}
public override string GetRequestedFileMimeType(HttpContext context)
{
return _ContentType ?? "application/octet-stream";
}
public override string GetRequestedFileEntityTag(HttpContext context)
{
return _EntityTag ?? string.Empty;
}
}
|
The two other methods are simply overridden to return the information acquired in the first without having to load the record again. This brings me to a caveat: for every partial request to the file, the record is reloaded. One could cache the information found and avoid this, but I’m not going to handle this here, as it is a potential security risk (you might no longer have access to the record in question, but this won’t be checked anymore).
So now we can support HTTP Range Units, and ADAM can be used to provide content to clients that demand it. I hope this information is as useful to you as it was for me, and feel free to leave comments!
Sample Code
The article contains sample code project(s).
You must be logged in to view or download sample code.
Sign in now