Skip to content

Instantly share code, notes, and snippets.

@clarmond
Forked from bennadel/code-1.cfm
Last active February 6, 2025 14:07
Show Gist options
  • Save clarmond/06dab343567db359239e0badf7f130eb to your computer and use it in GitHub Desktop.
Save clarmond/06dab343567db359239e0badf7f130eb to your computer and use it in GitHub Desktop.
Using ColdFusion To Stream Files To The Client Without Loading The Entire File Into Memory
<!---
Years ago I downloaded Ben Nadel's smartcfcontent.cfm to be able to stream video
from a protected area of an application. However, it did not work with
range requests which allows the user to scrub (fast forward) video.
This update shows how to do that.
Forked from https://gist.github.com/bennadel/9753119
--->
<cfif variables.isAuthorized>
<cfmodule template="smartcfcontent.cfm" type="video/mp4" file="#variables.filePath#" />
<cfelse>
<cfheader statuscode="401" statustext="Not Authorized">
</cfif>
<!--- Param the tag attributes. --->
<!---
This is the mime type of the content that we are
streaming to the browser.
--->
<cfparam
name="ATTRIBUTES.Type"
type="string"
default="application/octet-stream"
/>
<!---
This it the expanded path of the file that will be
streamed to the client.
--->
<cfparam
name="ATTRIBUTES.File"
type="string"
/>
<!---
Check HTTP request headers for "range".
Get start byte and send an HTTP 206 response code
--->
<cfset THISTAG.startByte = 0>
<cfset THISTAG.requestHeaders = GetHttpRequestData()["headers"]>
<cfif structKeyExists(THISTAG.requestHeaders, "range")>
<cfset THISTAG.fileInfo = getFileInfo(attributes.file)>
<cfset THISTAG.fileSize = THISTAG.fileInfo.size>
<cfset THISTAG.byteRange = listToArray(replace(THISTAG.requestHeaders.range, "bytes=", ""), "-")>
<cfset THISTAG.startByte = THISTAG.byteRange[1]>
<cfif arrayLen(THISTAG.byteRange) gt 1>
<cfset variable.startByte = THISTAG.byteRange[2]>
</cfif>
<cfheader statuscode="206" statustext="Partial">
<cfheader name="accept-range" value="bytes">
<cfheader name="content-length" value="#THISTAG.fileSize - THISTAG.startByte + 1#">
<cfheader name="content-range" value="bytes #THISTAG.startByte#-#THISTAG.fileSize-1#/#THISTAG.fileSize#">
</cfif>
<!---
Get a pointer to the response. We will need to this to
set the header values and finalize the data flush. To get
this, we will have to go two levels deep - past the text
output stream, to it's underlying binary stream.
--->
<cfset THISTAG.Response = GetPageContext()
.GetResponse()
.GetResponse()
/>
<!---
Get a pointer to the underlying binary repsonse stream
of the current ColdFusions.
--->
<cfset THISTAG.BinaryOutputStream = THISTAG.Response.GetOutputStream() />
<!---
We need to create a byte array that will be used to read
in the input stream and then transfer the input stream to
the output stream. Since ColdFusion doesn't have true
arrays, we need to hack one by grabbing the byte array
from a ColdFusion string.
Here, we are using the underlying Java method to grab a
byte array that is 5,120 bytes long (around 5 megs).
--->
<cfset THISTAG.ByteBuffer = RepeatString( "12345", 1024 )
.GetBytes()
/>
<!---
Now, we need to create a file input stream so that we can
read chunks of the file into memory as we stream it.
--->
<cfset THISTAG.FileInputStream = CreateObject(
"java",
"java.io.FileInputStream"
).Init(
JavaCast( "string", ATTRIBUTES.File )
)
/>
<!---
If start byte is greater than zero,
then jump ahead to that byte in the file stream
--->
<cfif THISTAG.startByte gt 0>
<cfset THISTAG.FileInputStream.skip(JavaCast("long", THISTAG.startByte))>
</cfif>
<!---
Before we start putting stuff in the buffer, let's
turn off the auto-flushing mechanism so that we have
full control.
--->
<cfset GetPageContext().SetFlushOutput(
JavaCast( "boolean", false )
) />
<!---
Reset the buffer to make sure nothing else has built up
in prior to this tag.
--->
<cfset THISTAG.Response.ResetBuffer() />
<!---
Set the content type using the mime type that was passed
in. This will give the browser information as to how to
deal with the streamed content.
--->
<cfset THISTAG.Response.SetContentType(
JavaCast( "string", ATTRIBUTES.Type )
) />
<!---
Now that we have all the elements in place, let's start
reading in the file and moving it to the output buffer.
We are going to keep doing this while until we hit the
end of the file.
--->
<cfloop condition="true">
<!--- Read a chunk of the file into the byte buffer. --->
<cfset THISTAG.BytesRead = THISTAG.FileInputStream.Read(
THISTAG.ByteBuffer,
JavaCast( "int", 0 ),
JavaCast( "int", ArrayLen( THISTAG.ByteBuffer ) )
) />
<!---
Check to see if any bytes were read. If not, then we
will have a -1 to denote that the end of the file has
been reached.
--->
<cfif (THISTAG.BytesRead NEQ -1)>
<!---
Write the buffer to the output stream. We want to be
careful only to write as many bytes as were read in.
--->
<cftry>
<cfset THISTAG.BinaryOutputStream.Write(
THISTAG.ByteBuffer,
JavaCast( "int", 0 ),
JavaCast( "int", THISTAG.BytesRead )
) />
<!--- Flush this new content to the client. --->
<cfset THISTAG.BinaryOutputStream.Flush() />
<cfcatch type="any"></cfcatch>
</cftry>
<cfelse>
<!---
We hit a (-1). We reached the end of the file. This
is not the cleanest solution, but just break out
of the loop.
--->
<cfbreak />
</cfif>
</cfloop>
<!---
ASSERT: At this point, we have fully read in the file,
moved it to the binary output stream, and then flushed it
to the client. Now, we just have to peform clean up work.
--->
<!---
Reset the response. This will clear any remaining information
in the buffer as well as any header information.
--->
<cftry>
<cfset THISTAG.Response.Reset() />
<cfcatch type="any"></cfcatch>
</cftry>
<!---
Close the file input stream to make sure we are not locking
the file from further use.
--->
<cfset THISTAG.FileInputStream.Close() />
<!---
Close the output stream to make sure no other content is
getting flushed to the browser.
--->
<cftry>
<cfset THISTAG.BinaryOutputStream.Close() />
<cfcatch type="any"></cfcatch>
</cftry>
<!---
Exit out of this tag to make sure it doesn't try to execute
for a second time if someone made it self-closing.
--->
<cfexit method="exittag" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment