DirectoryWatcher and ColdFusion Image Manipulation Example
Now that ColdFusion 8 gives us a crap load of image functions as well as event gateways in all editions, I thought I'd write up a super quick demo on how you can use both in your application. If you've never played with event gateways before, either because you thought they were too complex or you were running the Standard edition of ColdFusion, you should really take a look now. Event gateways are extremely powerful - but not as complex as you may think.
Before I begin - please check the docs on event gateways for a full explanation. I'm just writing a quick example here and won't be covering all the details.
The gateway I want to talk about today is the Directory Watcher. This gateway lets you, obviously, monitor a directory. You can have ColdFusion notice a change to the directory. This change can either be a new file, a modified file, or a deleted file.
So what are we going to do with our gateway? Our client, Hogwarts Press, Inc., has a group of reporters who handle press relations for the Hogwarts school. Being non-techies, they just want to take pictures. They can't be bothered to change these pictures for web publication.
To make things easier then we've set up a simple FTP connection for them to send their files to. They will download the pictures off their camera and FTP them up to a folder. (FTP is probably too much for them. You could also imagine an AIR application that lets them just drop files onto an icon.) The pictures will all be stored here:
/Users/ray/Documents/Web Sites/webroot/testingzone/dirwatcherimage/spool
Our code needs to:
- Check the file to ensure it is an image. (You never know with those non-techies.
- If an image, resize to a max width and height of 500 each. (Of course, you could also do other things like change the quality.)
- Move the image to a folder named 'ready.'
- If the file wasn't an image, delete it.
So let's start off by creating an instance of the DirectoryWatcher gateway. This is done in the ColdFusion Administrator. In order to do it, though, you need to specify a CFC and a configuration file. I created two empty files, watcher.cfc and config.ini. The figure below shows the values I set for my gateway. The name isn't important - but should reflect what your gateway is doing, or the application it is working with.

Now we need to edit the config file. This file is used by the event gateway to control the behavior of the code watching the directory. As I mentioned above - the gateway can notice adds, edits, and deletes. All I really care about is the add, so my config file looks like so:
# The directory you want to watch. If you are entering a Windows path
# either use forward slashes (C:/mydir) or escape the back slashes (C:\\mydir).
directory=/Users/ray/Documents/Web Sites/webroot/testingzone/dirwatcherimage/spool
# Should we watch the directory and all subdirectories too
# Default is no. Set to 'yes' to do the recursion.
recurse=no
# The interval between checks, in miliseconds
# Default is 60 seconds
interval=6000
# The comma separated list of extensions to match.
# Default is * - all files
extensions=*
# CFC Function for file Change events
# Default is onChange, set to nothing if you don't want to see these events
changeFunction=
# CFC Function for file Add events
# Default is onAdd, set to nothing if you don't want to see these events
addFunction=onAdd
# CFC Function for file Delete events
# Default is onDelete, set to nothing if you don't want to see these events
deleteFunction=
Notice the addFunction line. This simply says that the gateway should run the onAdd method of my CFC. Now let's take a look at the CFC:
<cfcomponent>
<cffunction name="onAdd">
<cfargument name="cfevent">
<cfset var myfile = arguments.cfevent.data.filename>
<cfset var image = "">
<cfset var newdest = getDirectoryFromPath(myfile)>
<cfif not isImageFile(myfile)>
<cflog file="dirwatcher" text="#myfile# is NOT an image">
<cffile action="delete" file="#myfile#">
<cfreturn />
</cfif>
<cflog file="dirwatcher" text="#myfile# is an image">
<!--- resize --->
<cfset image = imageRead(myfile)>
<cfif image.width gt 500 or image.height gt 500>
<cfset imageScaleToFit(image,500,500,"highestquality")>
<cflog file="dirwatcher" text="Resized to 500x500">
</cfif>
<cfset imageWrite(image, myfile)>
<!--- copy to ready --->
<!--- newdest is the same path as spool, so 'cheat' and switch to ready --->
<cfset newdest = replace(newdest, "/spool", "/ready")>
<cffile action="move" source="#myfile#" destination="#newdest#/#getFileFromPath(myfile)#">
<cflog file="dirwatcher" text="Moved to #newdest#/#getFileFromPath(myfile)#">
</cffunction>
</cfcomponent>
The first thing I want you to note is the argument: cfevent. When the gateway "talks" to my CFC, it will pass one argument, CFEVENT, that contains information about the event. In particular, the "data" key contains 3 values: filename, type, and lastmodified. The filename is obviously the filename. The type refers to the type of operation, and will either by ADD, CHANGE, or DELETE. Why is this even needed when I'm in an onAdd event? Nothing prevents me from pointing both the change and add functions to the same CFC method. My code could then check the value to see exactly what is going on.
So note then that I get the file out of the data. The rest of the code is rather simple. I check and see if the file is an image. If it isn't - I delete and leave the CFC. If it is - and if the image is too big - I resize it.
Note the use of cflog. All of this code runs behind the scenes. No web pages are viewed in this process. Therefore I used cflog so I could monitor what was going on.
As the web developer, what's nice is that I can now just look at my "ready" folder and put the web-ready images up on the web site.
As a few last notes:
- The code would be better if it handled name conflicts better.
- As mentioned, "web ready" means more than just shrinking. I was just trying to keep things simple.
- I mentioned FTP or an AIR app, but anything could drop files into this folder.
That's it. Hopefully folks find this useful!
Comments
Schweeet!
;-)
I tried to implement a feature similar to yours but ended up not doing it do to a problem I encountered (Windows Only Testing). In a scenario where FTP is involved a file is automatically placed in the folder when an upload is executed. Therefore, if your client is uploading a 25mb file and your directory watcher runs while only 10k has been uploaded it will set of an ADD EVENT. Of course at this point you would then try to run your code and then would fail because the file was not complete. The one good thing is that an event would be called again when the file finishes uploading (sends a CHANGE EVENT). Therefore while writing this message I realized hey why not just wrap a cftry around my function and if it fails well ignore and continue, since it would be called again on the CHANGE EVENT. Of course you would have to tie the same function to your ADD and CHANGE event and if that is ok then there is no problem.
Instead I just created a scheduled task that looks at the directory and compares the contents based on the last read and when it find a file which has not changed (upload complete) it runs the task.
See my task waits for a zip file containing an upload and a pdf form generated by the CRM. The app reads the zip makes sure contents are there, reads the pdf and based on the info provided in the pdf, enters info into db and moves the contents of the zip to the appropriate directory and deletes the zip uploaded.
My example can work with either the gateway or the scheduled task. Funny how sometimes you just need to type things out and the answer comes right back at you. Oh well, I already wrote both so either will work.
The Scheduled task does do it, the only thing is that there is up to a 3:00 minute delay for processing since I run it at 1.5 min intervals. Basically when you add a file and the ST runs it adds the file to a saved query and then will have to compare a minute and a half later. SO you could drop files into the folder but the processing time may take longer.
The thing is that if a file is corrupt / broken on upload and CF creates an error I log it and move the file to a rejectedUploads directory. So if it fails on the add event I may enter an unnecessary log entry (I am guessing I will not be able to move the file because it should be locked by the FTP process).
At this point I am bothered by the delay, so again with a little bit of typing I think what I will do is make sure to ignore a read error on the add event and place the processing CFC back to work on the EG rather than the ST.
The process should run again onChange event which at that point I will process all error properly. THe only thing I see happening here is probably having some files that failed on the add event and never ran an onChange event. WHich I can then just have a ST clean up once a day.
Going to test now .....

