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>
Archived Comments
Not too shabby. I haven't had a real use for downloading images yet in my applications, but I do have a project upcoming that this code could give me some ideas on.
Ideally what I will do is show all the images like your example then allow a user to click the image it will then bring it up in a JQuery Lightbox and then have a download link listed there to download each image. With your example I think I can take it one step further.
Just wanted to know what is the role of Temp Directory under \runtime\servers\coldfusion\SERVER-INF\temp\wwwroot-tmp in both the CFZIP example?
~
SanooP
It's just a temp directory.
Please help me with this.
I have an application, where I wanted to download dynamic images saved on a server. (CF 8)
Everything work fine on my local machine. But when I tried to dowload from server the zip file is blocked.
Even if I unblock it , gives me a warning like 'the compressed(zipped) files are invalid'
//Query to get teh file names.
<cfset VARIABLES.dirPath = "#ExpandPath('\')#path to server files\" />
<cfset VARIABLES.zipName = "Images.zip" />
<cfcontent type="application/zip" reset="true" />
<cfheader name="Content-Disposition" value="attachment;filename=#zipName#" >
<cffile action="read" file="#VARIABLES.dirPath#\#VARIABLES.zipName#" variable="archiveContent" />
//Zip Operation where source is the filename.
<cffile action="read" file="#VARIABLES.dirPath#\#VARIABLES.zipName#" variable="archiveContent" />
</cfcatch>
</cftry>
<cfoutput>#archiveContent#</cfoutput>
Thanks in advance.
The zip being blocked could be a network security thing, or IIS (or Apache), and this may not be a CF issue at all. I'd do this. Since your zip is on the server, try RDPing to the box and trying to open the zip there.
Btw - the code you have above is a bit more verbose then it needs to be. If you read the zip into a variable, you can pass that to cfcontent.