Streaming Binary Files in Chunks
Audience Level
Intermediate to advanced
Summary
A JScript ASP function to open a binary file (image, audio, video, etc) and stream it to the client browser without consuming too many webserver resources (specifically memory).
Why?
Conventional file streaming attempts to load the file into server memory first and then uses the default system buffering to write the content to the client. Loading the file into memory could be bad if the file is large and/or the server is busy and/or the server has low memory resources. Streaming files in chunks using the function below means that for large files there's a much reduced load on the server's resources, it also helps prevent timeouts during streaming for those on slow connections.
Instructions
Invoke as doStreamChunkedBinaryFile(strAbsFilePath, strMimeType, intKBChunkSize) where
“strAbsFilePath” is the absolute filepath and filename of the file
to stream to the client, e.g. “D:/website/images/pilchards.jpg”,
“strMimeType” is the appropriate mime-type for the content (e.g.
“image/jpeg”) and “intKBChunkSize” is the optional number of
bytes in KiloBytes in which to load chunks of the file into memory, and then write them to the client until the end of file.
Functions isNumber() and propError(), etc are defined in the JScript ASP library
and cacheControl() is defined separately (see link).
Source Code
Function: doStreamChunkedBinaryFile()
Description: Open a binary file (image, audio, video, etc) and stream it to the client browser without consuming too many webserver resources
Returns: n/a
History:
20070520 0048BST v1 Andrew Urquhart Created
*/
function doStreamChunkedBinaryFile(strAbsFilePath, strMimeType, intKBChunkSize) {
try {
if (!strAbsFilePath) {
throw new Error(1, "Expected parameter \"strAbsFilePath\" was undefined");
}
if (!strMimeType) {
throw new Error(2, "Expected parameter \"strMimeType\" was undefined");
}
if (!isNumber(intKBChunkSize)) {
var intKBChunkSize = 16; // Default to 16KB chunks
}
var intChunkSize = intKBChunkSize * 1024; // Bytes
// TEST IF FILE EXISTS
var objFSO = new ActiveXObject("Scripting.FileSystemObject");
if (!objFSO.FileExists(strAbsFilePath)) {
Response.Clear();
Response.Write("File Not Found");
Response.Status = 404;
Response.End();
}
// /TEST IF FILE EXISTS
// PERFORM HTTP 304 CACHE CONTROL - THIS BLOCK OPTIONAL
var objFile = objFSO.GetFile(strAbsFilePath);
var strFileName = objFile.Name;
var objDate = new Date(objFile.DateLastModified);
objFile = objFSO = null;
cacheControl(objDate, false);
// /PERFORM HTTP 304 CACHE CONTROL
// DOWNLOAD IN SMALLISH CHUNKS TO CONSERVE SERVER MEMORY
var objStream = Server.CreateObject("ADODB.Stream");
try {
Response.Clear();
Response.Buffer = false; // We're doing our own buffering instead
Response.ContentType = strMimeType;
objStream.Open();
objStream.Type = adTypeBinary;
objStream.LoadFromFile(strAbsFilePath);
Response.AddHeader("Content-Length", objStream.Size);
Response.AddHeader("Content-Disposition","attachment; filename=\"" + encodeURI(strFileName) + "\"");
// 3 minutes timeout or, if higher, the estimated download time on a 48Kb/s connection
Server.ScriptTimeout = Math.max(180, Math.ceil((objStream.Size * 8) / 48E3));
// Push 'intChunkSize' chunks to client. When near EOS, Read() will terminate at EOS regardless of being asked to read passed EOS
while (!objStream.EOS) {
Response.BinaryWrite(objStream.Read(intChunkSize));
Response.Flush();
}
Response.End();
}
finally {
objStream.Close();
}
// /DOWNLOAD
}
catch (err) {
propError(err, arguments);
}
}
Download
Note
Argh. On further inspection of memory resources the “LoadFromFile” method
appears to pre-load the entire file to memory, thus chunked reading and writing as in the above example
is probably less efficient than just than doing a read-all with “Read()”
and “BinaryWrite”'ing the whole thing to the client with ASP's “Response.Buffer = true”.
As yet I haven't found a way to partially load a file into memory, and until I do, or you do, there's
probably not much point in using the above main code example. You'd probably be much better off writing your own
component to do this and having it only read in a couple of packets worth of KB per read request in a loop until EOF.
You need to use this function before you have committed sending headers to the client. I.e. you need to
invoke it before any “Response.Write()” statements or before any other text/HTML
output.
If you encounter syntax errors with the ADO constants in your ASP pages (i.e. “adTypeBinary”)
then you need to add the ADODB Metadata Type library declaration to the top of your
“global.asa”, e.g.: