Symfony – file uploads with custom names and paths

I’ve been experimenting and doing a bit of digging with the Symfony file uploading in forms functionality. Out of the box, with eg. a Doctrine model, Symfony will save the filename only of an uploaded file, and it usually generates a random filename consisting of a hash of the original name together with a random number, and then appending the extension to the end. So your database entry ends up looking something like:

id: 1
name: Joe Bloggs
profile_picture: ffba81d3ac4ef1dcba70.txt

Great, if you’re not concerned about the appearance of the filename after uploading. If however, you want to preserve the filename, then you can create a generateFilename() method in your form object and Symfony will use that. But, what if you want to store more than the filename in your database, for example part of a path including multiple levels deep in directories? Or you want to do this each time, but don’t want to have to recreate/copy the same method multiple times? Or both?! Enter the use of sfValidatedFile.

In my experimentation, I wanted a custom file upload path, something like this:

  • /home/rich/myproject/web/uploads/entries/4cad77fd2da01/1/filename.txt
  • /home/rich/myproject/web/uploads/entries/4cad77fd2da01/2/otherfile.jpg

but I wanted to be able to store everything from 4cad… onwards as the filename, as everything up to ‘entries’ was a Symfony configuration value.

So, in your form configure() method, do something like the following:

// Generate your unique path here
$path = sfConfig::get("sf_my_upload_path") . DIRECTORY_SEPARATOR . uniqid();

$this->validatorSchema["myFileUpload"] = new sfValidatorFile(array("path" => $thisPath, "validated_file_class" => "myValidatedFile");

This tells Symfony to use a custom path to save, but also to use the myValidatedFile class to handle the processing of the uploaded file – move it to the correct place and so on. The myValidatedFile class is what we’re going to use to accomplish the above challenges.

Next, create the file lib/vendor/myValidatedFile.class.php:


class myValidatedFile extends sfValidatedFile
{
  public function generateFilename()
  {
    return $this->path . DIRECTORY_SEPARATOR . $this->getOriginalName();
  }

  public function save($file = null, $fileMode = 0666, $create = true, $dirMode = 0777)
  {
    $result = parent::save($file, $fileMode, $create, $dirMode);

    // Alter the savedName to reflect what we're after
    $this->savedName = str_replace(sfConfig::get("sf_my_upload_path") . DIRECTORY_SEPARATOR, "", $this->savedName);

    return null === $this->path ? $this->savedName : str_replace($this->path.DIRECTORY_SEPARATOR, '', $this->savedName);
  }
}

Simples. Let’s take each method in turn.

The first method simply returns the full path and original filename. Symfony will use the directory of this when deciding where to save the file, and it will use the leaf part to determine the file’s name. Part 1 accomplished!

The second method calls the original sfValidatedFile save() method. We’re not concerned with the mechanics of saving the file, so we’ll just do as normal (we’ve already set the path above). What we are concerned with is creating the correct value to be saved into the database or whatever you do with the field value eventually.

The key parts are the final 2 lines. Essentially here, we remove the part of the file’s path and filename that we have stored in the configuration value. This means that what is left will be (in the example above), our uniqid() value and the filename. This return value is what is saved/used elsewhere.

And then, your database entry will look like this:

id: 1
name: Joe Bloggs
profile_picture: 4cad77fd2da01/2/joes_picture.jpg

All you need to do now is set the “validated_file_class” option value on your file upload widget to myValidatedFile whenever you need this functionality, and job done!

2 Comments

  1. Posted 7 October 2010 at 9:15 pm | Permalink

    Don’t forget, that since you use the original-name (which is user-input in an upload-request and can be faked) and don’t filter the filename, bad requests can be send containing ../ in the filename and more.

    Ugly problems with umlaute, spaces, and other signs (not a-z0-9-_.) can happen to the filenames and with that to the generated urls.

    Missed that in my first toughts about changing symfonfys naming-scheme too ( http://www.robo47.net/blog/204-Stopping-symfony-from-screwing-up-uploaded-files-names ) but you can use either Doctrines urlize-method (used normally for slugs) which does transformation for umlaute and chars with accents and replaces other signs with hyphens.

    Another point you are missing now is name-collision, because even a single user can upload a file with the same name and will then overwrite an already existing image which can be associated with another db-entry, unless you create id-based directories or something like that.

    I wrote about that problem in Part 2.

    http://www.robo47.net/blog/206-Stopping-symfony-from-screwing-up-uploaded-files-names-PART-2

  2. Rich
    Posted 12 October 2010 at 11:22 am | Permalink

    @robo47 – thanks for commenting. Yes – the above is only meant as an example/proof of concept – obviously some form of sanitizing and checking the input should come as standard. Same with the ID-based directories – my use of uniqid() was used to demonstrate some random variable in the path. You can use a db entry ID or similar to give the unique-ness.

    And thanks for the interesting links! :-)

Post a Comment

Your email address is never published nor shared. Required fields are marked *

Ready to talk?

Whether you want to create a new digital product or make an existing one even better, we'd love to talk it through.

Get in touch