Hide
on 14/9/07

On a recent project I had to address the requirement that the admin user can "upload any type of file and associate it with any of the models in the system". In reality this seems to be a rather logical and common requirement. For quite a while now I've been collecting bits of info on how to upload files with cake and it's about time to begin sharing the knowledge. Did I say sharing? I meant I'll show you what I know, and hope you show me how to do it better :)

Partly because I am really loaded at the moment, but also due to the number of files which are 'supporting' the behavior are numerous, this blog will not follow the format of my previous posts. The code which to be discussed is in the noswad CakeForge project

Preamble

A massive shout out to both Tane Piper (Digitalspaghetti) for the original upload behavior and Jon Bennet (JBen) for his insightful Image helper which, as you may note from the download, forms the basis for the image manipulation code in the ImageUpload behavior.

The code has been taken from a running project, but I have not yet commented the code due to time constraints but in any case the code may change - Particularly as the code relies on the patch attached to Ticket 3178 to work.

I'll write a tutorial on the bakery if the code settles down after taking on board whatever suggestions come forth.

What's In it?

The Upload Behavior

There are a lot of files in the download but the most important inclusion is the upload behavior.

This file allows you to save meta data about your file uploads in the database and store the files wherever you configure the uploaded files to go.

Control of uploads by mime type, extension and size

Validation related to file uploading itself is handled by the behavior (dependent on ticket 3178).

The behavior will only be active for file uploads, as such if you wanted to edit the meta data that is collected you can via a normal form.

Image Upload Behavior

This file demonstrates clear separation from file upload handling and image manipulation. In addition to everything it inherits from the upload behavior it also saves width and height data to the database (meta) table if the fields are present and has two functions defined to permit resizing of a file that has been uploaded or any arbitrary image file which it is passed. After resizing a file either the file is written to the path defined in the parameters, or the image contents are returned to the calling method.

Generic App Model

The associations defined in this file permit a thumb image to be associated with any model instance, and any number of files in general to be associated and used as you wish.

Attachment administration controller and views

Lets you upload files and by default stores files in APP/uploads. Files are not directly stored in the webroot.

Using the attachment controller, you can add and view all files that are uploaded.

The model and id to which the attachment will be associated is dependent on the url parameters passed. To upload a file for Blog number 22 you would go to the url /admin/attachments/add/Blog/22. Submit the form and it's up there for use.

File and Image handling controller

The functionality within this controller could just as well be put in the attachments controller, but it is presented here separately.

The routes file that is in the download will route requests for /files/* and /img/* to this controller. On the fly image resizing is also built into this controller. "On the fly image resizing?" you say? Consider the following example, see the source for more info:

  • As an admin user you go to the url /admin/attachments/add/Blog/22 and upload a file named "test.jpg" and give it a description of "what happens here". The file uploaded is 3000px by 5000px. Users :D!
  • You go to, or include in a page, the url /img/Blog/22/test-what-happens-here.jpg . The image you would see in this case is served at the default size, which if you don't change the code means you will see your image restricted to 300px wide and 1000px high, as you don't want it changing size (most likely) what you will therefore get is your image served as 300px x 500px.
  • You want to have a thumb of the same image so you go to, or include in a page, the url /img/50x50/Blog/22/test-what-happens-here.jpg. Tada, you have your thumb.
  • You decide that the default size for this image is too small, so you go to, or include in a page, the url /img/600x1000/Blog/22/test-what-happens-here.jpg. Tada, you have your image as 600px x 1000px.
  • For uses to be able to see/download the original file you can go to, or include in a page, the url /files/Blog/22/test-what-happens-here.jpg. Tada, you have your image as originally uploaded, 3000px x 5000px.

For none-image files the logic is slightly different:

  • As an admin user you go to the url /admin/attachments/add/Blog/22 and upload a file named "music.mp3" and give it a description of "great tune this one".
  • In this case it isn't an image, and if you include a link, or go to an 'image' url like /img/50x50/Blog/22/music-great-tune-this-one.mp3 you will get..... redirected to the url /img/types/mp3.png if the file exists or /img/types/generic.png if not
  • To access the original file you include a link, or go to the url /files/Blog/22/music-great-tune-this-one.mp3

And also...

A few other things are there in the code ;) but I'll leave describing them for another opportunity.

Wrapping Up

The source files described here allow you to upload files, restricted by extension, mimetype or size and associate them with any model in your application. Meta data is stored in the database while the files are stored out of the webroot. Files can be dynamically served/copied to the webroot upon demand.

Bake on!

28 Responses to Generic File Upload Behavior

  1. 1
    Your say. Nice work Ad7six, just wanted to say that my version of the ImageHelper is a reworking of an existing helper from the Bakery here http://bakery.cakephp.org/articles/view/image-resize-helper - don't want to take credit for others work! cheers, jon
  2. 2

    Excellent stuff! I'll use it in my next project and let you know what i run into.

  3. 3

    How's that for a slice of fried gold:

    Behavior methods can now be used as validation callbacks without any hacking. I wonder how else I can (ab)use that now :D

  4. 4

    Hey AD7six,

    I started working on combining the Image/Upload behavior just two days before I saw your post about your combination of them here (Whereas my behavior wasn't as innovative as yours).

    It seems that you and I started from the same codebase but headed into different directions. I decided to rewrite the behavior once again. I really liked your idea of attaching documents to models and the way you access documents thru the routes.

    I'm nearly finished with it right now but will be on vacation in venice for the next 7 days. If you would like to see the code just tell me.

    -David

  5. 5
    Hey thanks for this, its really useful. Just one recommendation - I'm developing on a windows box (unavoidable unfortunately) and I need to change line 34 of the images controller to read $destination = WWW_ROOT . str_replace('/', DS, $this->here); because it was having some dificulty with the / directory seperator. Thanks again!
  6. 6

    David: Sure, stick it in the paste bin (as a saved paste) and add a link to it here, maybe there can be some synergy found.

    Jetpac: Good call, I'll update the controller with that.

    PS. If you don't want to link to a site, just leave it blank ;).

  7. 7

    Hey I'm really liking this functionality :)

    Just another couple of lines I had to change to get things running well on windows in case anyone else is in the same situation:

    1 - the dirFormat field in the upload behavior works best with / instead of DS because its used in the urls.
    2 - then we need to convert the / back to DS in the images controller for viewing: (line16) $dir = str_replace(DS, '/', implode(DS, $args));

    seems like url is a required field in the comments.

    cheers,
    J

  8. 8

    Just one more thing Andy: your example app seems to be using two behaviors whodunnit and slug to handle converting urls and storing who made edits. I'm sure they're not too complicated but I think I would find them useful. If you could share them that would be great :)

  9. 9

    Hi Jetpac, the slug behavior is on the bakery, I'll have a think about publishing the whoDunnit behavior (I don't like showing things where I break cake's rules :D)

  10. 10
    Hey Andy! what a great job! I tested your demo app and I had to apply Jon's patches (read above). I found the following: when I delete a pic-attachement the thumbs don't get deleted pics renaming does not work as expected (i.e. pic-filename_pic-description) but the result is "_pic-description If in my post content I put an img tag like (src="img/Post/X/img-name") a the original pic is copied there Additionaly just one thought: while the automagic of the pic resizing according to the uri passed is a really cool idea, isn't this dangerous if somebody knows the trick and wants to burn all your disk space? Cheers again for sharing! Dan
  11. 11

    Hi Cakefreak,

    I see I really do need to fix that link field!

    There is a controller method included which will clear out the webroot, so that stale files can be removed, as is it will clear out everything but could easily be adapted to just delete versions of one file at a time. I should point out that the controller/resizing solution is only a suggestion and demonstration of use. As you rightly point out it could be abused to form the basis for a DOS attack, I did think about taking some of the ideas from, for example the secure get component such that you can't just type whatever size you want and get it. Alternatively it could be made to work via requestAction only and after saving the data a call is made to create only the appropriate files.

    I don't quite follow regarding the renaming, if you edit the filename, there is nothing in place to rename the existing file, is that what you mean?

  12. 12
    Hey Andy, cheers for the fast replay! Well what I wanted to say about the files renaming stuff is the following. When I normally add a picture to a post I would expect (#20 Uploadbehavior [upload.php] --> 'fileFormat' => '{$filename}_{$description}') the uploaded picture to be renamed as *fileformat* describes. I only get the uploaded file renamed as _{$description}. Maybe this is just what it is expected! I'll play around with your Generic Upload Behaviour abit more and I'll let you know! Dan
  13. 13

    Hi Cakefreak,

    Are you experiencing that with the demo applicaiton or in your own code? What you describe is the concequence of the code being unable to determine what the var $filename is.

  14. 14

    Hey andy,
    in the demo app I downloaded from CakeForge

  15. 15

    Hi Cakefreak,

    Well I found you a reason: path info only returns the filename since 5.2.0. so you just need to substitute where that is used to be functionally equivalent.

  16. 16

    Hey Andy, thanks a lot for the tip!

    There were another couple of features not working properly (some paths to the images + the stuff/link to clean the directories), but I'll be back with more precision!

    Have a nice weekend!

    Dan

  17. 17
    Hello. Great work. I have been playing with the sample application and found a behavior that don't know if it is normal. When you add more than one attachments to a "modelX" item and try to findAll() items of "modelX", then I get duplicated rows. I tried $this->Banner->unbindModel(array('hasMany' => array('Attachment')), array('hasOne' => array('Thumb'))); but it didn't work. Any clue? Thanks
  18. 18
    Great, very nice piece of code. I think i'd be better to check and replace some characters that can be entered in the description field (and the filename), such as whitespace -> _, ñ -> n, á -> a, and so on. thank you!
  19. 19

    @Cakefreak: I don't usually use windows, but I'll see if I can install and check it out. The lack of a link to clean the webroot was deliberate - Didn't want to confuse anybody who didn't bother to read the code and hopefully after reading the code you know what it does.

    @Mauricio: I think there is a simple mistake in the demo code which causes adds to insert a blank row. I'll update that when I get a minute (I was experimenting with means of not having an add method since it is often a needless duplication)

    @Mariano: Oops. That was taken care of in a previous version. Looks like I optimized it out :D. I'll optimize it back in.

  20. 20
    Hey Andy, about the filename not showing up (only the description get stored -> read previous comments) for those not using PHP 5.2 This is my solution: replace line #295 upload.php with: $filename = substr($info['basename'], 0, strlen($info['basename'])-4);//DAN HACK Dan
  21. 21

    Of late, I'm getting "No Database table for model AppModel (expected app_models), create it first." error with recent 1.2 svn branch.

    Do you have any comments on that?

  22. 22

    Another note that the source of the above problem is AppModel::__construct() .

    If we remove this definition app_model.php, the error disappears.

  23. 23
    With regard to the problems with the AppModel::__construct(): I have a problem with this if the model that uses (actsAs) the Generic Upload Behavior has an habtm association. How can I sort this out? Cheers Andrew. Dan
  24. 24
    Any clue about my last comment?
  25. 25

    Hi Dan,

    Whoops I hadn't put the fix which I discussed on the google group with R. Rajesh Jeba Anbiah in the download.

    Did it just now, you might kick yourself as it's quite simple ;).

    Cheers,

    AD

  26. 26
    Cheers Andy! I'm back again hoping to save somebody else from panic! I was using the image_upload behaviour with an iFrame for multiple images upload, and IE7 was filing. In fact IE7 trietes mimes a bit differently: image/jpeg - image/pjpeg (.jpg) image/x-png - image/png (.png) Therefore the settings should be updated to: $this->__defaultSettings['allowedMime'] = array('image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png', 'image/bmp'); Hope this will avoid somebody helse panic! Dan
  27. 27
    I know that using windows is probably not the best choice for developing a web app, but I have to do so, at least at work. One thing made me crazy while playing around with this behaviour: the mkdir() function of folder.php will not create directorys in a recursive way, I had to manually change line 480 in lib/folder.php and add a 'true' parameter to the mkdir function: if (mkdir($pathname, intval($mode, 8), true)) { Otherwise the dynamic creation of different filesizes doesn't work. (In windows!) No problems on my MAC with that though. I thought I share my experience with you. Thanks for all your great articles! -B.
  28. 28

    Hi Andy, I just wanna say thanks for the great job. Your approach for a generic Upload Behavior is really cool and far useful. I was traying to do something similar but I had trouble thinking the right model. This fits every need I had in an elegant and easy way. I'm extending it to support also video. I'll let you know how it works out!

    Regards!