-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!--- | |
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!--- 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