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.
2 Name: <input type="text" name="name"><br/>
3 Email: <input type="text" name="email"><br/>
4 Attachments: <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.
Comment 1 written by Raymond Camden on 11 November 2009, at 12:27 PM
Comment 2 written by Henry Ho on 11 November 2009, at 12:50 PM
Comment 3 written by Raymond Camden on 11 November 2009, at 12:52 PM
Comment 4 written by Jon Hartmann on 11 November 2009, at 1:09 PM
Comment 5 written by Tyler Clendenin on 11 November 2009, at 1:24 PM
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.
Comment 6 written by Raymond Camden on 11 November 2009, at 1:36 PM
Comment 7 written by Russ S. on 11 November 2009, at 4:58 PM
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.
Comment 8 written by Raymond Camden on 11 November 2009, at 5:01 PM
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.
Comment 9 written by Henry Ho on 11 November 2009, at 5:01 PM
Comment 10 written by Russ S. on 11 November 2009, at 7:33 PM
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.
Comment 11 written by Raymond Camden on 11 November 2009, at 10:20 PM
Comment 12 written by Paul on 12 November 2009, at 7:25 AM
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
Comment 13 written by Raymond Camden on 12 November 2009, at 8:03 AM
Comment 14 written by Raymond Camden on 12 November 2009, at 8:08 AM
Comment 15 written by Paul on 12 November 2009, at 8:33 AM
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
Comment 16 written by Raymond Camden on 12 November 2009, at 8:46 AM
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. :)
Comment 17 written by Pablo Varando on 12 November 2009, at 10:03 AM
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
Comment 18 written by rouademunte on 13 November 2009, at 2:31 PM
Comment 19 written by Ben Nadel on 17 November 2009, at 8:45 AM
Comment 20 written by Raymond Camden on 17 November 2009, at 8:50 AM
<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.
Comment 21 written by Ben Nadel on 17 November 2009, at 8:56 AM
Comment 22 written by Jon Hartmann on 17 November 2009, at 10:03 AM
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?
Comment 23 written by Raymond Camden on 17 November 2009, at 10:15 AM
Comment 24 written by Henry Ho on 24 November 2009, at 7:40 PM
url="index.cfm?event=general.myUploadEvent&#urlEncodedFormat(session.urltoken)#"
Comment 25 written by Henry Ho on 24 November 2009, at 8:02 PM
Comment 26 written by Raymond Camden on 24 November 2009, at 8:02 PM
Comment 27 written by Sebastiaan on 1 December 2009, at 2:34 AM
Thanx up front ;-)
Comment 28 written by Raymond Camden on 1 December 2009, at 5:02 PM
If I've never blogged on it here, let me know and I will try to.
Comment 29 written by Richard Gilbert on 2 December 2009, at 1:52 AM
Comment 30 written by Raymond Camden on 4 December 2009, at 9:27 AM
Comment 31 written by Pete Freitag on 4 December 2009, at 10:05 AM
Comment 32 written by Raymond Camden on 4 December 2009, at 10:27 AM
uploadall.cfm?#urlEncodedFormat(session.urltoken)#
to
urlSessionFormat('uploadall.cfm')
Comment 33 written by Pete Freitag on 4 December 2009, at 10:30 AM
Comment 34 written by Raymond Camden on 4 December 2009, at 10:59 AM
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?
Comment 35 written by Pete Freitag on 4 December 2009, at 12:52 PM
Comment 36 written by Raymond Camden on 4 December 2009, at 12:58 PM
Comment 37 written by Steve Caldwell on 10 December 2009, at 2:32 PM
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">
Comment 38 written by Raymond Camden on 10 December 2009, at 2:40 PM
Comment 39 written by Steve Caldwell on 10 December 2009, at 3:01 PM
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?
Comment 40 written by Raymond Camden on 10 December 2009, at 3:08 PM
Comment 41 written by Steve Caldwell on 10 December 2009, at 3:13 PM
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
Comment 42 written by Raymond Camden on 10 December 2009, at 3:14 PM
Comment 43 written by Pete Freitag on 10 December 2009, at 3:18 PM
Comment 44 written by Raymond Camden on 10 December 2009, at 3:26 PM
Comment 45 written by Steve Caldwell on 10 December 2009, at 4:59 PM
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?
Comment 46 written by Raymond Camden on 10 December 2009, at 5:07 PM
Comment 47 written by Steve Caldwell on 11 December 2009, at 8:08 AM
Comment 48 written by Raymond Camden on 11 December 2009, at 8:13 AM
Comment 49 written by Steve Caldwell on 11 December 2009, at 8:20 AM
Comment 50 written by Raymond Camden on 11 December 2009, at 8:21 AM
[Add Comment] [Subscribe to Comments]