File upload with CakePHP

In this post I will describe how to upload and store a file in the database. Yes, you read correct, I will describe how to store a file in the database. I know, a lot of people do not recommend storing files in a database due to performance reasons. Of course, this is an important point you have to consider when you design your application. The reasons why I store files often in the database are:

  • all data are stored in one place: the database
  • it is easier to test
  • it is easier to develop as I do not have to keep database and file system in sync

Enough of bla-bla, let us dive in the code. First the table definition for MySQL:

CREATE TABLE files (
  id INT(11) NOT NULL AUTO_INCREMENT,
  name VARCHAR(75) NOT NULL,
  type VARCHAR(255) NOT NULL,
  size INT(11) NOT NULL,
  data MEDIUMBLOB NOT NULL,
  created DATETIME,
  modified DATETIME,
  PRIMARY KEY (id)
);

Note: Use MEDIUMBLOB or LONGBLOB as data type unless you know for sure that the file size of the uploaded files is never bigger than 64kB.

The model is straight-forward:

// app/models/file.php
class File extends AppModel
{
    var $name = 'File';
}

We omit the controller for the moment and create directly the view:

// app/views/files/add.thtml
<form action="/files/add" enctype="multipart/form-data" method="post">
    <?php echo $html->file('File'); ?>
    <?php echo $html->submit('Upload')); ?>
</form>

So, now we ready to build the controller and to implement the add() function:

// app/controllers/files_controller.php
class FilesController extends AppController
{
    function add()
    {			
        if (!empty($this->params['form']) && 
             is_uploaded_file($this->params[’form’][’File’][’tmp_name’]))
        {
            $fileData = fread(fopen($this->params['form']['File']['tmp_name'], "r"), 
                                     $this->params['form']['File']['size']);
            $this->params['form']['File']['data'] = $fileData;
					
            $this->File->save($this->params['form']['File']);

            $this->redirect('somecontroller/someaction');
        }
    }
}

Easy, isn’t it? Up to now we have stored the file in the database. To retrieve the file from the database, we need a download() action which we add to our controller:

function download($id)
{
    $file = $this->File->findById($id);
		
    header('Content-type: ' . $file['File']['type']);
    header('Content-length: ' . $file['File']['size']);
    header('Content-Disposition: attachment; filename='.$file['File']['name']);
    echo $file['File']['data'];
			
    exit();
}

Well, I know that this action is probably not very cake-like, the proper way would be to use a layout and a view, but this way I have less to write ;-)

So, that’s it. We have finished our very simple upload/download application.

Update (2006-08-05): Fixed a security hole in the code above, see also “Be careful with file uploads”. Thanks to Lamby.

30 Comments

  1. auto
    Posted April 15, 2006 at 3:04 pm | Permalink

    Pretty interesting! It seems like you are saying it’s best for development and testing. Do you have a method to convert back once that is done? And would you recommend something like this in a live app, like a way to keep people from leeching files by linking directly to them?

  2. Posted April 15, 2006 at 6:19 pm | Permalink

    No, I don’t do any conversions, i.e. in the live application are the files stored in the database, too. Otherwise it would be useless to test a functionality you do not use in the live application ;-)

  3. Posted April 17, 2006 at 11:26 am | Permalink

    A View is there to define the presentation of the data to the user. With a raw bytestream such as a file, there is no ‘presentation’ to be defined…

  4. Lex Slaghuis
    Posted July 5, 2006 at 3:56 pm | Permalink

    I am having some difficulties retrieving binairies correctly ( the image gets jibbered or the pdf is corrupt). Text files work perfectly.

    I use the exact code as provided! First i open the add page in the browser and upload the file. Then I open the http://……/files/download/1 page manually. (instead of 1 a possible id number of the file)

    It doesn’t work on both IE and firefox. My server is a WAMP, latest version on a windows 2003 server.

    I suspect the issues to depend on corrupt decoding through the http protocol.

  5. Lex Slaghuis
    Posted July 5, 2006 at 4:55 pm | Permalink

    issue resolved;

    bug in the add function!
    insert addslashes method for direct database insertion….
    $fileData = addslashes(fread(fopen($this->params[‘form’][‘File’][‘tmp_name’], “r”),
    $this->params[‘form’][‘File’][‘size’]));

  6. Posted July 6, 2006 at 5:56 pm | Permalink

    @Lex Slaghuis: I am glad you could resolve the issue. I think you have to use the “addSlashes” function if you use different encodings. In my application I use UTF-8 everywhere, and I don’t experienced any problems.

  7. Posted July 12, 2006 at 4:08 am | Permalink

    Let’s say I wanted to add the field product_id to the files table as a foreign key to the products table. How would this look in function add() {}?

  8. Posted July 13, 2006 at 3:49 pm | Permalink

    @Shane Shepherd: If the product_id is in the same form as the file upload is you do not have to add anything. Otherwise you have to do something like:

    $this->params[‘form’][‘File’][‘product_id’] = $productId;

  9. Posted July 13, 2006 at 11:10 pm | Permalink

    @cakebaker – Sweet! You rock! Thanks!

  10. Posted July 19, 2006 at 7:36 pm | Permalink

    Hmm. What would you think would happen if uploaded a form of my own something along the lines of:

    Assuming there was a mechanism for obtaining uploaded files, I would then have your mySQL password, something very dangerous on a shared host. Enjoy :)

  11. Posted July 19, 2006 at 7:38 pm | Permalink

    [input hidden=”data[File][tmp_name]” value=”/var/www/cake/app/config/database.php” /]
    [input hidden=”data[File][size]” value=”9999999″ /]

    (It ate my previous post’s HTML tags, so reposting with square brackets. Sorry)

  12. robdeman
    Posted July 31, 2006 at 12:42 am | Permalink

    I had trouble with corrupted JPEGs. I turned out that the view file that displays the image upload form was using a layout file with a ‘wrong’ Byte Order Marker, it was set to ‘dos’. Most text editor can change this to ‘unix’. This resolved my corrupted images.

    used on: WinXP, WAMP server, Firefox

  13. MawciKurL
    Posted August 1, 2006 at 6:00 am | Permalink

    I’m a newbie in CakePHP. I want to upload Excel files but not to store it in the database, I just want to read them. Can I do this without creating a model? And can a model created without a database table pair?

  14. Posted August 1, 2006 at 9:17 am | Permalink

    @MawciKurL: You do not need a model for file uploads.

    I am not sure if I understand your second question correctly. But if your question was “is it possible to create a model which doesn’t use a table”, than the answer is yes. You have to add the following snippet to your model:
    var $useTable = false;

  15. Posted August 4, 2006 at 12:16 am | Permalink

    Just to elaborate on my previous two comments, you are leaving a large security hole open if you do not check the filenames with file_is_uploaded() or similar. Regards, Lamby

  16. Posted August 5, 2006 at 2:20 pm | Permalink

    @Lamby: I have fixed it in the post.

  17. JP
    Posted August 5, 2006 at 11:51 pm | Permalink

    Great article, just what I am looking for, but I’m having a problem when I try to implement this in my project (complete newbie, sorry!).
    I’ve copied into my project and the view add.thtml loads fine, I select a file and press upload but I get an object not found error “The requested URL was not found on this server.”, but I know it is there because it refers to itself! Am I missing something very obvious?
    Cheers, JP

  18. Posted August 8, 2006 at 10:58 am | Permalink

    @JP: Hm, maybe you have to change the action property in your add.thtml file?

  19. elswidi
    Posted August 10, 2006 at 3:16 pm | Permalink

    thanks cakebaker for this helpful example. I want to use it as a milestone and build on top of it – ie. adding more inputs in “add.thtml” !?
    I am facing problems trying to add an extra text input as such :
    input(‘Modelname/fieldname’, array(‘size’ => ’50’))?>

    I believe the problem is because the following 2 lines:
    $this->params[‘form’][‘Site’][‘data’]=$siteData;
    $this->Site->save($this->params[‘form’]);

    the data that is to be saved is only about the image !!

    any idea on how to modify the add() fuinction in the controller so that it accepts extra params from the form in add.thtml??

  20. elswidi
    Posted August 10, 2006 at 10:53 pm | Permalink

    I know that the question might sounds silly but i am really a newbie :)
    Anyway… I solved it with in very odd way. I discovered that I can retrieve post data from my form in my controler in from 2 Arrays. First is params[‘form’] which is used in the example. Second is params[‘data’]. That was the key of my solution since ‘data’ – which is the image- is overwritten by $this->params[’form’][’Site’][’data’]=$siteData;
    So, I decided to do the following: use params[‘data’] to save extra inputs and params[‘forms’] to save the image.

    here is exactly what I did:
    //app/controllers/files_controller.php
    function add()
    {
    //first save all form data except the image using params[‘data’]
    if (empty($this->params[‘data’]))
    {
    $this->render();
    }
    else
    {
    if ($this->Site->save($this->data[‘Site’]))
    {
    $this->redirect(‘sites/index’);
    }
    }

    // socnd save the image using params[‘forms’]
    // same code in the example
    if (!empty($this->params[‘form’]) && is_uploaded_file($this->params[‘form’][‘Site’][‘tmp_name’]))
    {
    $siteData = fread(fopen($this->params[‘form’][‘Site’][‘tmp_name’],”r”),$this->params[‘form’][‘Site’][‘size’]);
    $this->params[‘form’][‘Site’][‘data’]=$siteData;
    $this->Site->save($this->params[‘form’]);
    $this->Site->save($siteData);
    $this->redirect(‘sites/index’);
    }

    }

    hope might be useful but still not convinced with it !!?

    Any ideas??

  21. Posted August 11, 2006 at 2:32 pm | Permalink

    @elswidi: Hm, I don’t know why you use two saves:
    $this->Site->save($this->params[’form’]);
    $this->Site->save($siteData);

    With something like:
    $this->params[‘form’][‘Site’]’otherdata’] = $this->data[‘Site’][‘otherdata’];
    $this->params[’form’][’Site’][’data’]=$siteData;
    $this->Site->save($this->params[‘form’][‘Site’];

    it should be possible to save all your data with just one save instead of three saves.

  22. warren
    Posted August 27, 2006 at 3:24 pm | Permalink

    Hi im followed your instructions here on this post. and all worked well with my localserver. however, when im downloading the image from my site on a remote server, the image went crappy. im not sure where the problem lies.

    if the problem is with my code, then it shouldnt work in my localserver in the firstplace. i doubt if its cakephp’s code because otherwise you would encounter the problem too.

    thanks

  23. Posted August 30, 2006 at 7:59 am | Permalink

    For all users that encounter the same problem as warren, the solution can be found in http://groups.google.com/group/cake-php/browse_thread/thread/bf5a3c382b69d599/8f1b0453ffd13640

  24. warren
    Posted September 13, 2006 at 12:30 pm | Permalink

    yeah, i just did the addslash function. its now ok. thanks

  25. Evan
    Posted September 19, 2006 at 9:10 am | Permalink

    I got a problem here. The image file was uploaded to the database alright. And my database client app is able to display the image correctly (w/o need for addslashes). But when I tried to download it, I get a corrupted file. When I checked with the hex editor, there were two extra space characters at the beginning of my file.

    Did anyone else encounter this kind of problem?

  26. Posted September 23, 2006 at 8:02 am | Permalink

    The cause of Evan’s problem was a space after ?>, see http://groups.google.com/group/cake-php/browse_thread/thread/bf5a3c382b69d599/9789d4562797df1c

  27. KesheR
    Posted September 25, 2006 at 12:24 pm | Permalink

    But there is no problem storing files in the database if you use the extremely useful cakecaching

  28. Posted October 5, 2006 at 1:16 pm | Permalink

    please help! My images get corrupted when I download them from MySQL! I am using the addslahes function. Looks like there is some small things that have changed from the original file. (hex)??

  29. Posted October 5, 2006 at 3:25 pm | Permalink

    @Asbjørn Morell: If you use the addslashes function when storing the file, you have to remove them when you retrieve the file from the database.

  30. go
    Posted December 14, 2006 at 8:34 am | Permalink

    I had the same problem with JP. At first, it loads fine. But when i click “Upload” button, I got an error that says “The requested URL was not found on this server.”


%d bloggers like this: