Yesterday I shared a demo of using CFZIP with ColdFusion. In my demo application. users could upload images one at a time or a zip of images. The demo made use of cfzip to read the zip file and extract out the images. In today's blog entry, I'm going to modify the demo to allow you to download multiple images. I'll make use of cfzip to generate a zip file on the fly and then serve it to the user.
To begin, I had to make a slight modification to the display portion of the demo. I don't like it when blog entries get off topic and distract yo uwith things that are off topic, but I'm going to break that rule just a bit here. In the first edition, I used the "media grid" feature of Bootstrap. (If you haven't noticed yet, I'm a huge fan of Bootstrap for making my ugly demos far less ugly.) Unfortunately, that grid didn't work well when I added in checkboxes. So I switched to a normal grid. This required a bit more logic in order to close out the divs right, but the important thing to note here is the use of a checkbox for each image. Here is a snippet:
<h2>Images</h2>
<form method="post">
<cfloop index="x" from="1" to="#arrayLen(thumbs)#">
<cfset image = thumbs[x]>
<cfoutput>
<cfif x is 1>
<div class="row">
</cfif>
<div class="span8" style="text-align:center">
<a href="photos/#getFileFromPath(image)#" class="imageList">
<img class="thumbnail" src="thumbs/#getFileFromPath(image)#">
</a><br/>
<input type="checkbox" name="download" value="#getFileFromPath(image)#">
</div>
<cfif x mod 2 is 0>
</div>
<cfif x lt arrayLen(thumbs)>
<div class="row">
</cfif>
</cfif>
</cfoutput>
</cfloop>
<cfif arrayLen(thumbs) mod 2 is 1>
</div>
</cfif>
<input type="submit" name="downloadaction" id="downloadbutton" value="Download" style="display:none" class="btn primary">
</form>
Note that I have a submit button that is hidden. I made use of some jQuery to hide/show the button whenever you've selected at least one item:
$("input[name='download']").change(function() {
var sel = $("input[name='download']:checked");
if(sel.length > 0) $("#downloadbutton").fadeIn(250);
else $("#downloadbutton").fadeOut(250);
});
Here's a quick look:
Now let's look at the code that handles this form submission:
<cfzip action="zip" file="#dest#">
<cfloop index="f" list="#form.download#">
<cfzipparam source="#imageDir#/#f#">
</cfloop>
</cfzip> <cfheader name="Content-disposition" value="attachment;filename=download.zip" />
<cfheader name="content-length" value="#getFileInfo(dest).size#" />
<cfcontent type="application/zip" file="#dest#" reset="true" />
</cfif>
<cfif structKeyExists(form, "downloadaction") and structKeyExists(form, "download")
and len(form.download)>
<cfset dest = getTempDirectory() & "/" & createUUID() & ".zip">
First - note the CFIF check. It not only looks for the submit button but also the download field. In theory it's not possible to submit the form if you haven't picked anything, but you should always follow up client side validation with server side validation. I create a destination in the temporary directory. I then use cfzip to create the zip file based on the images you selected. You can cfzip an entire folder if you want, or specify individual files. You can also rename the files in the archive itself. Our use is simpler though.
Once created, the zip is served up with a combination of cfheader and cfcontent tags. The two header tags tell the browser we are downloading an attachment with the name download.zip. Providing the content length also helps the browser let the user know how long the download will take. Finally, the cfcontent tag serves up the actual binary data.
That's it. The entire template of the CFM is below, and I've attached a zip with the complete application.
<!--- images and thumbs dir are relative --->
<cfset imageDir = expandPath("./photos") & "/">
<cfset thumbDir = expandPath("./thumbs") & "/"> <!--- used to flag if we uploaded crap --->
<cfset successFlag = false> <cfif structKeyExists(form, "upload") and len(form.upload)>
<cfset tempDir = getTempDirectory()>
<cffile action="upload" filefield="upload" destination="#tempDir#" nameconflict="overwrite"> <cfset theFile = file.serverdirectory & "/" & file.serverfile> <cfset images = []> <cfif file.filewassaved>
<cfif isImageFile(theFile)>
<cfset arrayAppend(images,theFile)>
</cfif> <!--- check for zip --->
<cfif file.serverfileext is "zip">
<cftry>
<cfzip action="list" filter=".jpg,.png,*.gif" file="#theFile#" name="files">
<cfloop query="files">
<cfzip action="unzip" entryPath="#name#" destination="#tempDir#" file="#theFile#" overwrite="true">
<cfif isImageFile(tempdir & "/" & name)>
<cfset arrayAppend(images, tempdir & "/" & name)>
</cfif>
</cfloop>
<cfcatch>
<cfdump var="#cfcatch#">
</cfcatch>
</cftry>
</cfif>
</cfif> <cfif arrayLen(images)>
<cfloop index="theFile" array="#images#">
<!--- create a UUID based name. Helps ensure we don't conflict --->
<cfset newName = createUUID() & "." & listLast(theFile, ".")>
<!--- copy original to image dir --->
<cfset fileCopy(theFile, imageDir & newName)>
<!--- now make a thumb version --->
<cfset imgOb = imageRead(theFile)>
<cfset imageScaleToFit(imgOb, 200,200)>
<cfset imageWrite(imgOb, thumbDir & newName)>
</cfloop>
<cfset successFlag = true>
</cfif> </cfif> <cfif structKeyExists(form, "downloadaction") and structKeyExists(form, "download")
and len(form.download)>
<cfset dest = getTempDirectory() & "/" & createUUID() & ".zip"> <cfzip action="zip" file="#dest#">
<cfloop index="f" list="#form.download#">
<cfzipparam source="#imageDir#/#f#">
</cfloop>
</cfzip> <cfheader name="Content-disposition" value="attachment;filename=download.zip" />
<cfheader name="content-length" value="#getFileInfo(dest).size#" />
<cfcontent type="application/zip" file="#dest#" reset="true" />
</cfif> <cfset thumbs = directoryList(thumbDir,true,"name",".jpg|.png|*.gif" )> <!DOCTYPE html>
<html>
<head>
<title>Zip Demo</title>
<meta http-equiv="Content-Type" content="text/html;charset=ISO-8859-1" /> <link rel="stylesheet" href="http://twitter.github.com/bootstrap/1.4.0/bootstrap.min.css">
<link rel="stylesheet" href="jquery.lightbox-0.5.css" type="text/css" />
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
<script type="text/javascript" src="jquery.lightbox-0.5.min.js"></script>
<script type="text/javascript">
$(function() {
$(".imageList").lightBox(); $("input[name='download']").change(function() {
var sel = $("input[name='download']:checked");
if(sel.length > 0) $("#downloadbutton").fadeIn(250);
else $("#downloadbutton").fadeOut(250);
});
});
</script>
</head>
<body> <div class="container"> <h2>Images</h2>
<form method="post">
<cfloop index="x" from="1" to="#arrayLen(thumbs)#">
<cfset image = thumbs[x]>
<cfoutput>
<cfif x is 1>
<div class="row">
</cfif>
<div class="span8" style="text-align:center">
<a href="photos/#getFileFromPath(image)#" class="imageList">
<img class="thumbnail" src="thumbs/#getFileFromPath(image)#">
</a><br/>
<input type="checkbox" name="download" value="#getFileFromPath(image)#">
</div>
<cfif x mod 2 is 0>
</div>
<cfif x lt arrayLen(thumbs)>
<div class="row">
</cfif>
</cfif>
</cfoutput>
</cfloop>
<cfif arrayLen(thumbs) mod 2 is 1>
</div>
</cfif>
<input type="submit" name="downloadaction" id="downloadbutton" value="Download" style="display:none" class="btn primary">
</form> <h2>Upload New Image</h2>
<form enctype="multipart/form-data" method="post"> <cfif successFlag>
<p>
Image(s) have been uploaded. Thanks!
</p>
</cfif> <cfif structKeyExists(variables, "errors")>
<cfoutput><p>#variables.errors#</p></cfoutput>
</cfif> <p>
Select image, or zip file of images:
<input type="file" name="upload">
</p> <p>
<input type="submit" value="Upload" class="btn primary">
</p> </form> </div> </body>
</html>