I keep a note of possible blog ideas and as I've been having a bit of writer's block lately, I thought I'd take a look and see what I could turn into a decent article. One of the older ideas I had was something that I thought would be a good example. Imagine you've built an image uploader for your site and have a bit of processing on the back end (scaling, gray scaling, etc). Now imagine you want to allow for bulk uploads of zip files to do the same thing. Here is a simple one page example of that in action.
First, I'll share the entire template, and then I'll walk through the various parts. Note - in a typical MVC framework this would be split up. You get the idea.
<cfif structKeyExists(form, "upload") and
structKeyExists(form, "zip")>
<!--- first, where to store? outside of web root of course --->
<cfset uploadDir = getTempDirectory()>
<cftry>
<cffile action="upload" destination="#uploadDir#" nameconflict="makeunique"
result="result" accept="application/zip" filefield="zip">
<cfif result.fileWasSaved>
<cfset theFile = result.serverDirectory & "/" & result.serverFile>
<!--- destination will be temp folder + uuid --->
<cfset tempFolder = getTempDirectory() & "/" & createUUID()>
<cfdirectory action="create" directory="#tempFolder#">
<!--- unzip it --->
<cfzip action="unzip" destination="#tempFolder#" file="#theFile#">
<!--- now go through, find images, resize and save in the right place --->
<!--- folder for our nice sized images, probably better as an Application var --->
<cfset imageDestination = getDirectoryFromPath(getBaseTemplatePath()) & "/images">
<!--- get all the images, well JPG and PNG. Docs are wrong about filter. Multiple work. --->
<cfset files = directoryList(tempFolder, false, "name", "*.jpg|*.png")>
<cfloop index="aFile" array="#files#">
<!--- Ok, for each one, read it, scale it, copy it --->
<cfset img = imageRead(tempFolder & "/" & aFile)>
<cfset imageScaleToFit(img, 200, 200)>
<cfset imageWrite(img, imageDestination & "/" & aFile, true)>
</cfloop>
<cfset message = "Success, you uploaded #arraylen(files)# images.">
<!--- clean up --->
<cfset directoryDelete(tempFolder,true)>
</cfif>
<cfcatch>
<!---
<cfdump var="#cfcatch#" label="cfcatch">
--->
<!--- This could be done nicer. --->
<cfset message = cfcatch.detail>
</cfcatch>
</cftry>
</cfif>
<cfif structKeyExists(variables, "message")>
<p>
<cfoutput>#message#</cfoutput>
</p>
</cfif>
<form method="post" enctype="multipart/form-data">
Select a zip:
<input type="file" name="zip" accept="application/zip" required><br/>
<input type="submit" name="upload" value="Upload">
</form>
Whenever I build a simple self-posting form like this I start with the UI at the bottom and put the processing on top. Again - you would normally have the logic in a controller. Please remember that.
In our form, we've got a grand total of two items - the file upload and the submit button. Note that I make use of the accept attribute and required attribute for the input field. This provides simple validation for the form. It isn't enough, of course, but why bother writing JavaScript on top when at least some of my audience will get it for free (and more every day as they move to newer browsers)?
The processing logic is pretty direct. We begin by checking to see if you've uploaded a file - and then we process that upload with the cffile tag. Note the use of the temporary directory. Never - ever - upload to a web accessible folder.
Next we make a new temporary folder to store the extracted zip. We unzip it and then filter out on images (in my case, JPG and PNG, you could include GIF as well). We then loop over the images, load them as an image object, scale (the value there was arbitrary), and save.
That's it. Here's the result of a zip I tested.
While this works, it does take a little bit of time. Image scaling can be an expensive operation on the server. If you want to get fancy, you can try simply threading the operations in the background:
<cfset files = directoryList(tempFolder, false, "name", "*.jpg|*.png")>
<cfloop index="x" from="1" to="#arraylen(files)#">
<cfset aFile = files[x]>
<cfthread name="scaleImage#x#" theFile="#aFile#">
<!--- Ok, for each one, read it, scale it, copy it --->
<cfset img = imageRead(tempFolder & "/" & attributes.theFile)>
<cfset imageScaleToFit(img, 200, 200)>
<cfset imageWrite(img, imageDestination & "/" & attributes.theFile, true)>
</cfthread>
</cfloop>
This runs significantly faster, but could be made more reliable in terms of reporting.
I've attached both CFMs as a zip to this blog entry. Happy Friday!