I've done a few blog entries on ColdFusion 9's new multi file uploader. But for a while now I've wanted to build, and share, a complete example. As I've said before, putting a multi file uploader on your page is simple. Incredibly simple. Unfortunately, using the uploader is non-trivial. You've got multiple things going on at once that you have to manage. It is doable (I wrote up this demo in approximately 30 minutes), but you certainly need to do your planning ahead of time. So what will my little application do?
My application is a portfolio submission form. You can imagine a budding young artist bucking for a job at a top creative agency in the thrilling burg of Lafayette, LA. (It's thrilling - honest.) In order to apply for a job, s/he has to submit both biographical information as well as examples of their work. The application will take the user's information and their files and email it to the creative director. Right off the bat there you can see that we're going to need a form that mixes both traditional fields and the fancy new awesomeness of the multi file uploader.
I began by creating a simple form asking for 3 bits of biographical information:
<form action="portfoliosubmission.cfm" method="post">
Your Name: <input type="text" name="name" value="#form.name#"><br/>
Your Email: <input type="text" name="email" value="#form.email#"><br/>
Your Bio:<br/>
<textarea name="bio">#form.bio#</textarea><br/>
Notice that I'm using predefined form values for the three fields. They were set with a cfparam on the top of my template. (The entire code base is available via the download link below.) Next I added my multi file uploader:
<cffileupload extensionfilter="jpg,jpeg,gif,png,bmp,tiff" name="portfoliofiles" maxfileselect="5" title="Portfolio Images" url="fileupload.cfm?#urlEncodedFormat(session.urltoken)#">
I want you to notice a few things here. First, both the extensionfilter and maxfileselect are totally arbitrary. I used image file extensions because this is meant for a design job submission. I picked 5 because I have 5 fingers on one hand. The URL points to a separate CFM. That CFM will handle processing the uploads. Notice: Due to a bug in how the control is created, you must add the current Session URL token to the URL. If you do not, the upload request will be done with a new session.
Ok, so we've got a basic form. What's going to happen when the user actually picks some files to upload? Well since we are going to be emailing these files, we don't need to keep them around forever. I think using the new Virtual File System would be an excellent place to store those files. I added the following code to my onApplicationStart method of my Application.cfc:
<cffunction name="onApplicationStart" returnType="boolean" output="false">
<cfset application.portfolioUploadRoot = "ram:///portfoliouploads">
<cfif not directoryExists(application.portfolioUploadRoot)>
<cfdirectory action="create" directory="#application.portfolioUploadRoot#">
</cfif>
<cfreturn true>
</cffunction>
As you can see, I've got an application variable that points to a path on the VFS. I then see if that directory exists and if not, I create it. Most likely it will never exist when the application starts, but I tend to rerun onApplicationStart manually during testing, and frankly, it doesn't hurt to be anal.
So now we have a root folder for our uploads, but obviously we may have more than one person using the form at once. I next created an onSessionStart that would make a unique subdirectory just for one person.
<cffunction name="onSessionStart" returnType="void" output="false">
<cfset session.myuploadroot = application.portfolioUploadRoot & "/" & replace(createUUID(), "-", "_", "all")>
<cfif not directoryExists(session.myuploadroot)>
<cfdirectory action="create" directory="#session.myuploadroot#">
</cfif>
</cffunction>
This method creates a new subdirectory using the Application's root folder and a new UUID. Like before, this folder will not exist, but I couldn't help going the extra step and wrapping it with a directoryExists().
So at this point, we have a safe storage place for the files. One that is unique per user. If we look at fileupload.cfm, we can see that it is rather trivial:
<cfif structKeyExists(form, "filedata")>
<cffile action="upload" filefield="filedata" destination="#session.myuploadroot#" nameconflict="overwrite" result="result">
<!--- optional post processing --->
</cfif>
<cfset str.STATUS = 200>
<cfset str.MESSAGE = "passed">
<cfoutput>#serializeJSON(str)#</cfoutput>
Two things to note here. I'm not doing any post-processing of the files. You may want to. In my case, I'm just going to leave them be. You should not trust that the user sent image files even with the extension filter. That being said, I'm not storing the files or executing them. I'm just emailing them. Secondly, and this is critical and not documented - be sure to output JSON with a 200 status. Big thanks to Brian Rinaldi and his blog post on the topic. If you don't have this, one file upload will work but the multi file uploader won't continue on to the next file.
Alright, so we've got the file uploads working, now let's circle back and look at how my form validates. I'm doing everything client side for simplicity's sake:
<cfif structKeyExists(form, "submit")>
<cfset form.name = trim(htmlEditFormat(form.name))>
<cfset form.email = trim(htmlEditFormat(form.email))>
<cfset form.bio = trim(htmlEditFormat(form.bio))>
<cfset errors = []>
<cfif not len(form.name)>
<cfset arrayAppend(errors, "You must include your name.")>
</cfif>
<cfif not len(form.email) or not isValid("email", form.email)>
<cfset arrayAppend(errors, "You must include a valid email address.")>
</cfif>
<cfif not len(form.bio)>
<cfset arrayAppend(errors, "You must include your bio.")>
</cfif>
<cfdirectory action="list" name="myuploads" directory="#session.myuploadroot#">
<cfif myuploads.recordCount is 0>
<cfset arrayAppend(errors, "You must upload at least one file.")>
</cfif>
<cfif arrayLen(errors) is 0>
<cfmail to="someone@myorg.org" from="#form.email#" subject="Portfolio Submission">
From: #form.name# (#form.email#)
Bio:
#form.bio#
<cfloop query="myuploads">
<cfmailparam file="#session.myuploadroot#/#name#">
</cfloop>
</cfmail>
<cfset showForm = false>
</cfif>
</cfif>
So I begin by doing real simple validation on the 3 text fields. None of that should be new. But then check out how I validate if the user uploaded anything. I simply do a directory list on their personal storage. If it is empty, it means they didn't upload any files. Finally, if there are no errors, I send the email out. Notice how I use that previous query to create my list of attachments. I use both the errors and the directory list back on the bottom of the form when it is redisplayed:
<cfif structKeyExists(variables, "myuploads") and myuploads.recordCount>
<p>
You have uploaded the following files already: #valueList(myuploads.name)#.
</p>
</cfif>
<cfif structKeyExists(variables, "errors")>
<p>
<b>Please correct the following error(s):</b>
<ul>
<cfloop index="e" array="#errors#">
<li>#e#</li>
</cfloop>
</ul>
</p>
</cfif>
Now it's time for the final part of the puzzle - clean up. Remember that a user may upload files and never actually hit submit on the form itself. I use both onApplicationEnd and onSessionEnd to remove the files from the VFS:
<cffunction name="onApplicationEnd" returnType="void" output="false">
<cfargument name="applicationScope" required="true">
<cfif directoryExists(arguments.applicationScope.portfolioUploadRoot)>
<cfdirectory action="delete" recurse="true" directory="#arguments.applicationScope.portfolioUploadRoot#">
</cfif>
</cffunction>
<cffunction name="onSessionEnd" returnType="void" output="false">
<cfargument name="sessionScope" type="struct" required="true">
<cfargument name="appScope" type="struct" required="false">
<cfif directoryExists(arguments.sessionScope.myuploadroot)>
<cfdirectory action="delete" recurse="true" directory="#arguments.sessionScope.myuploadroot#">
</cfif>
</cffunction>
For the most part none of that code should be new to you, but do notice how you never directly access the Session or Application scope within these methods. They are always passed by reference instead.
So that's it. There are a few things that could be done to make it fancier of course, but hopefully this gives you a complete example of what it means to add a multi file uploader to your form. Questions and comments are definitely welcome.