Here's my solution, I'd be interested to hear how others have done this. It seemed like a little bit overkill as I was writing it, but I also realized that I would never need to write this snippet of code again, so that kind of makes up for the long-windedness of it.
The solution is composed of all the standard files and snippets in a Model Glue application.
- the "model" CFC - in this case, a simple object
- a .cfm view file where the "cfcontent" resides
- a function in the Controller.cfc to create the download object and add it to the event
- a message broadcast, listener, and event handler in the ModelGlue.xml file
- all the views, which compose my page and contain the links to download the file
First, let's look at the "Download" Bean CFC: Download.cfc represents my "file" object that stores my file information. It basically just stores values for the file name, file path, and mime type of the file I'm serving for download. It lives in my "model" folder.
<!--- Download.cfc --->
<cfcomponent displayname="Download" hint="retrieves a file for download to be served with cfcontent">
<!--- init() --->
<cffunction name="init" access="public" returntype="any">
<cfargument name="fileName" type="string" required="yes" hint="name of file once downloaded">
<cfargument name="filePath" type="string" required="yes" hint="full path of file to be served">
<cfargument name="fileType" type="string" required="yes" hint="mime type of file">
<cfset filename =" ARGUMENTS.fileName">
filepath =" ExpandPath(ARGUMENTS.filePath)">
filetype =" ARGUMENTS.fileType">
<!--- getters --->
<cffunction name="getFilename" returntype="string" access="public" output="false">
<cffunction name="getFilePath" returntype="string" access="public" output="false">
<cffunction name="getFileType" returntype="string" access="public" output="false">
Notice that the CFC uses ExpandPath() so that a relative url can be passed in as the location of the file, which is much easier when it comes time to build your link, and then the app is not server-dependent. Yes, I could write setters and use those setters to construct the CFC, but I'm never going to build these objects to say, insert into a database, so I didn't bother.
Then I have the actual "download" include. It's called _download.cfm because the underscore preceding the filename is a common convention for files that should not be called directly, but need to be included in another file, it also differentiates it clearly from your "template" view files.
This file gets dynamically included on the page when the "download" viewState value is present. It also only serves the file if it's not a simple value (to ensure the values we need are there). It lives in the "views" folder. Notice there are separate values for the file path and the file name. The file name is what the user sees when they download the file, the file path tells CF where to locate the file. Being able to set the file name in the header is nice because you can use a short, sweet and simple file name for the end user, but your own convoluted, datestamped , purpose-coded file naming convention for the actual file (ok maybe that's just me).
Also we use the http header so that the user will be prompted with the browser "download" dialog, rather than leaving it to the browser to try and open the file directly. It prevents us from having to list some weird instructions like "right-click and choose Save Target As" for the less-savvy users (arent' they all).
<cfset download =" viewState.getValue('download')">
<cfif NOT isSimpleValue(download)>
<cfheader name="Content-Disposition" value="attachment; filename=#download.getFileName()#">
<cfcontent type="#download.getfiletype()#" file="#download.getfilePath()#" deletefile="no">
My Model Glue Controller.cfc contains this function to build the Download object. It just calls the Download.cfc, creates the "download" object, and then adds the instance of said object to the event so it can be found when the page is rendered.
<!--- Controller.cfc --->
<cffunction name="getMyDoc" access="public" returntype="void" output="false">
<cfargument name="event" type="any" required="yes">
<cfset fileDownload = createObject("component","model.Download").init('MyDoc.pdf','assets/docs/MyDoc_123.pdf','application/pdf')>
<cfset arguments.event.setValue("download", fileDownload)>
In My ModelGlue.xml file, I hook my application into the above function with this XML snippet.
<message-listener message="getMyDoc" function="getMyDoc" />
The event handler for that message is...
<message name="getMyDoc" />
<result do="page.mydoc" />
The broadcast, obviously, requests the download object via the Controller.cfc. The result - "do" is the same page that my link is on - so the user does not leave that page when they click the "download" link, they are just served the file. Lastly, on the page where I want the link that is clicked to download this file, this is the <a> tag:
<a href="#viewState.getValue('myself')#download.mydoc">Download My Doc</a>
Nice. That's all there is to it.
Of course this sample doesn't show any security or anything, this would only be appropriate for a public download. Sorry if the spacing and indentation is ugly, Blogger's code formatting is a pain! Hope you find it helpful!