Posted in ColdFusion | Posted on 11-11-2009 | 7,885 views
Note - the following applies to any multi file uploader - not just the one that ships with ColdFusion 9. Earlier today I read a great article by Dan Vega about some of the new UI controls in ColdFusion 9. One of them was the new multi file uploader. If you haven't had a chance to play with it, I highly encourage you take a quick look at the demos Dan uses in his article. Reading his article brought up some interesting things that I think people need to be aware of before making use of the control. I had a long talk with Dan about this, and unfortunately, the more we talked, the more we discovered that there are quite a few non-trivial things you need to concern yourself with before adding this feature to your site.
The primary, and most important, thing to keep in mind is that the multi file uploader performs a HTTP post by itself. Consider the following simple form.
2Name: <input type="text" name="name"><br/>
3Email: <input type="text" name="email"><br/>
4Attachments: <cffileupload url="uploadall.cfm" name="files" ><br/>
5<input type="submit">
6</form>
This example is different than what you see typically demonstrated with the control. However, it represents a more real world scenario. Users don't normally just upload files by themselves, but in context with other data as well. In this case my form has 3 "fields", a name, a email, and a attachments area. Our business logic needs to work with the data as whole. It will probably want to insert the two simple values into a table. It will probably then want to log the files attached to the submission in another table, where that tables links back to the original.
Right off the bat you can see the problem. The files post to uploadall.cfm. The form itself posts back to form.cfm. So on top of that - guess what - we have other problems as well. What if the user uploads files and never hits submit? What if the user picks files and never hits the upload button? As you can see - the more we approach a real world scenario, the less trivial this "simple" feature becomes. Let's talk about some possible solutions.
First - let's focus on how we can handle keeping the files together with the rest of the form submission. We have to accept that - at minimum - we may or may not have files waiting when the form is submitted. We need to also accept that we may have multiple people using the same form. Because of this, it may make sense to use a file location based on a value unique to the session. So for example:
This creates a UUID based variable stored within the session scope. I called it "storage" which is a bit vague. If I had 2 or more forms on my site then I'd maybe want to use a prefix:
We can then use this for our uploadall.cfm file.
2 <cfdirectory action="create" directory="ram://#session.storage#">
3</cfif>
4
5<cffile action="upload" destination="ram://#session.storage#" nameconflict="makeunique"/>
Now that I have a place to store my files, I can look in that folder when I submit the rest of the form. However, this leads to more problems. (Does anything in life go from complex to simple??) First, what happens if I go to the form, upload some files, change my mind, come back and decide to start over? Most likely I want the form itself to remove all files that exist within my own storage directory. This means a simple page reload will also cause the storage folder to empty out. Secondly - what happens if I upload some files - go eat lunch - and then come back and complete the form? Well all of a sudden my storage system is empty. My form will think that no files were associated with the submission. If a form didn't require a login, then this could lead to confusion. What I'd probably suggest here is a warning if the form is submitted long after it was initially created. (You can use a hidden form field with the current time for this.) Then the user can decide if they need to bother to do the form again. Using this system also allows us to make use of onSessionEnd to empty out the folder when the session expires - again only if the user never bothers to submit the form. If they do we should process the folder immediately.
And unfortunately - we have another problem as well. When the multi file uploader sends data to the server, it does NOT pass along cookies. What does that mean? It means it uses another session. Luckily we can get around that by appending the session values to the URL:
Note the use of urlEncodedFormat. This is required! Session.URLToken is URL safe already, but for some reason when I left this off, only the CFID portion was sent to the server.
Now that you have wrapped your brain around that issue - let's bring up another thing to consider. What if the user selects a few files, but never hits Upload? Unfortunately, there is no JavaScript API that let's you look at the list of files to see if anything is there. You are basically out of luck. The best I can recommend here is to put a nice message next to the control and by the submit button as well. Of course, users hate to read, but at some point they need to take some responsibility!
Finally - let me leave with two quick things I found while testing. First - if you use onComplete and forget to actually write the JavaScript function, the multi file control won't load at all. Firefox correctly reported an error, but with it "hidden" in the Error Console (boy that really bugs me - I wish Firefox would add something to to the status bar on errors) I completely missed this at first. Secondly - if you do not use the cffile action="upload" on your form process, then you will never get a response in the form control. I guess this is to be expected, but I thought I could simply ignore the file uploads while testing. Nope - when I had a blank file, the form control waited forever for a response. Again - not surprising.


BTW, I would be negligent if I did not suggest the firebug addon for firefox. It does in fact give you a message in the status bar telling you all about JS errors.
Also, why doesn't CF doesn't send cookie data along with the uploads? Unless there's some good reason, I'd consider that a bug that needs fixing.
And I don't know if I agree with you about storing uploaded files in RAM. Too many simultaneous uploads and your virtual memory is toast.
Definitely agree on it being a bug that cookies aren't sent.
RAM: Ram is cheap. ;) But certainly you could also save it to some other physical place.
RAM: Storing user uploads in it just seems like an unnecessary risk. If hackers didn't already have enough ways to screw your server, now they can eat up your RAM too.
One problem I have identified relates to the cffile upload info array created. The CF9 documentation page for the tag states that 'After a file upload is completed, this tag creates an array of structures specified by the result parameter. Each structure in the array contains upload result information for one file'.
Unfortunately similar to someone who has added a comment to the info page, only the last element appears to be accessible, hence all the useful file details are lost, making subsequent manipulation and association a bit problematic
I am very impressed that you are on the case so early - it's 14:14 here in the UK!
I picked up that the upload runs almost like a cfloop hence the over writing of the cffile array perhaps.
As it runs like a loop I was hoping to pick out info from the cffile array or some form of counter variable as I am trying to rename the uploaded files at the same time. This covers the situation where a number of totally different filenames can be made into an identifiable set e.g.
front.jpg, side.jpg, top.jpg becomes elevation_01_01.jpg, elevation_01_02.jpg, elevation_01_03.jpg
At the moment I am working on the idea of throwing the uploaded files into a temp dir, interrogating this directory to get the file details, saving that in a structure within the session scope, processing said files (modifying my structure accordingly) then storing key items from this structure in the 'master' db record relating to this one process. Oh, then moving the modified files to my permanent image directory and deleting the original temp dir.
I was hoping that getting into the cffile array at time of upload as it loops through might have cut down the amount of code and hence been a bit more elegant.
Regards
Paul
I'm still planning on a _full_ demo of this feature, and I want to demo the simpler version I described above.
FYI, it's not so early here. ;) I was up at 5:45AM, my kids not much longer. :)
So if you have let's say "upload images" I create a gallery, then on each gallery item I provide an upload option... when you upload the images (for example) I tie them to the gallery, then, and only then, do i allow you to edit the details for the image. (There are other uploaders out there that alow you to have those fields with the image uploads so you can do it all in one, but the one in CF9 does not have that feature). A good example of this would be the one that FaceBook ad MySpace uses...)
Your first example assumes you want to do it in the traditional way (one action page to do it all)... but by leveraging the flash interface you are in fact doing two things (hence why this happens with ALL of them)... I agree it would be nice to have an interface that selects multiple-files, but gets submitted with the form, instead of in a two step process...
P
<img src="foo.jpg" hover="foo_hot.jpg">
Sure it's trivial in JS, but mouseover images have been in use since my grandparents wrote HTML (*cough*) so it's a bit odd that simpler support for it hasn't been added.
Besides, in what case would you want to hover an image to see a different one in which you would not also need JS to control placement, so a caption, or some other related thing?
url="index.cfm?event=general.myUploadEvent&#urlEncodedFormat(session.urltoken)#"
Thanx up front ;-)
If I've never blogged on it here, let me know and I will try to.
uploadall.cfm?#urlEncodedFormat(session.urltoken)#
to
urlSessionFormat('uploadall.cfm')
uploadall.cfm?CFID%3D10508%26CFTOKEN%3D99378812
I then got rid of urlencodedformat:
uploadall.cfm?CFID=10508&CFTOKEN=99378812
Then turned back on J2EE, kept the formatting off, and got:
uploadall.cfm?CFID=10508&CFTOKEN=99378812&jsessionid=8430d08da42160276820f3c7013e57281487
These last 2 look valid. So perhaps it is just a bug in my code with urlEncodedFormat?
Nevertheless, I still am prompted, and if I cancel the prompt, get a 401 error. If I type my credentials, the uploader hangs and crashes the browser.
Anyone run into any permissions stuff with this tag? Pretty sure my code is ok, but here it is anyway.
<cffileupload url="dbaction.cfm?uuid=#url.uuid#&addfiles=1" bgcolor="767676" width="750" maxuploadsize="19" extensionfilter="*.pdf, *.xls, *.xlsx, *.doc, *.docx, *.rtf, *.ppt, *.pptx" />
<!---dbaction.cfm--->
<cffile action="uploadall" destination="c:\temp">
Glad this happened though, did I uncover a bug? Apparently CF doesn't tell you when you try to upload to a non-existent page...Wouldn't it make more sense for a 404 to be thrown?
Could be useful for the dev who is having brain trouble that day, unless I'm the only one who struggles with that from time to time :P
I also needed to pass the UUID to my handler page so it knows what to name the folder...but it only reads the first parameter after the ? in my url
I may be getting my terms mixed up, so here's what I mean:
url="./handlers/file_manager.cfm?uuid=#url.uuid#&addfile=1" only sends the uuid, and "url="./handlers/file_manager.cfm?addfile=1&uuid=#url.uuid#" only sends addfile=1...
I guess I'll have to create separate pages for each action?
Any ideas of what I might check? (BTW, I also found out the hard way about it only sending 1 parameter across, even if you sent more)
Then came CF9, and now it doesn't work; we originally thought that a Firefox update was behind the issue, but today while testing and looking in my IIS logs, I see that the POST requests do indeed reach IIS, and the URL carries the correct session CFID and CFTOKEN vars. But when logging to the administrator log files, the actual Session.CFID and Session.CFTOKEN variables end up with different values... basically, Flash's own session.
Perplexed as to why the URL variables were now being ignored, whereas they weren't before, I found this blurb part way down the Adobe CF9 Client State Management docs (http://tinyurl.com/2c6jyfy):
Note: The behavior is as follows when CFID and CFTOKEN are provided in the URL: If session exists, the CFID and CFTOKEN from the URL are ignored. If the session does not exist, CFID and CFTOKEN from the URL are used to validate the session and the session is used if it is valid. If the session is not valid, a new session is created. CFID and CFTOKEN are regenerated.
So, it would seem that CF9 changed it's behavior from CF8, where the URL variables overrode any other session variables. That seems odd to me, but it's the best I could come up with today... let me know if you know any differently, Ray!
I'll let you know if the Session.urltoken stuff makes a difference, but I'm guessing that this change just makes CF9 more secure, and it won't? Not necessarily a bad thing, just a change! ;)
I need to change pages after the files are uploaded
Sample:
<cfset request.theURLVars = urlEncodedFormat("teamid=#event.getvalue('teamid',0)#&linkid=#event.getvalue('linkid',0)#") />
<cffileupload progressbar="true" stoponerror="true" maxfileselect="20" title="Photo Gallery Image Upload"
extensionfilter = "jpg,png,gif" deletebuttonlabel="Remove from upload list"
clearbuttonlabel="Clear upload list" url="/upload/uploadPhotoGalleryProcess.cfm?#request.theURLVars#"/>
Ray, if you want to just edit the entry and delete this comment, that's fine.
<cffileupload url="uploaddocs.cfm" progressbar="true" name="myupload" addButtonLabel = "Add File" clearButtonlabel = "Clear it" hideUploadButton = "false" width=600 height=400 title = "Add New Document" BGCOLOR="##FFFFFF" MAXFILESELECT=10 UPLOADBUTTONLABEL="Upload Files" stopOnError=false/>
uploaddocs.cfm
<cfscript>
thisPath = ExpandPath("*.*");
thisDirectory = GetDirectoryFromPath(thisPath);
FileDir = thisDirectory & "uploads";
DocRoot = request.root;
RootDir = GetDirectoryFromPath(DocRoot);
</cfscript>
<cfif not DirectoryExists(FileDir)>
<cfdirectory action="create" directory="#FileDir#" >
</cfif>
<cffile action="upload" filefield="fileData" destination = "#FileDir#" nameconflict="makeunique" mode="777">
url="someurl.cfm?#urlEncodedFormat(session.urltoken)#"
then i just added it and it worked.
Thank's
I've recently encountered this blog and it's assets for the CFFileupload. Does anyone have any suggestions for multiple file download? i.e. CFFiledownload?
Many Thanks!
[Add Comment] [Subscribe to Comments]