This is difficult to do with Zend for a few reasons.
- If you rename the file after it's been moved to the upload destination then it might've overwritten a file that you didn't want rewritten.
For example, say you have a destination directory called /path/to/my/pics. If two users, at the same time, upload a picture called 'me.png' then they might override each other. This is because the rename filter is applied AFTER the file is moved to /path/to/my/pics. Thus, it might not be renamed before it is overwritten by a new file upload.
- If you use Zend's rename filter then you can't keep the original files extension.
The way I did it was to do the following,
1. Extend the http transfer adapter to pass the rename filter the original file name. The normal http transfer adapter passes in the temporary name in the tmp directory and does not have the file extension.
- Extend the rename filter so that you can specify whether or not it should keep the original file extension.
Afterward, you'll have to add the prefix to the form you're using so that the form can find your adapter and so your adapter can find the new Rename filter you've made.
The reason I did it this way is because my destination directory was going to have one pic in it for every user where each pic was named 'user1.jpg' or 'user2.png'. I wanted to rename the file at the same time that I moved it so it wouldn't override any other files in the directory that I wanted to keep.
Here is the code I've used.
class My_File_Transfer_Adapter_Http
extends Zend_File_Transfer_Adapter_Http
{
/**
* Receive the file from the client (Upload)
* This differs from the Zend adapter in that
* the adapter passes in the files actual
* name to the rename filter so that when
* it is renamed, the renamer can use the extension
* of the file and keep it or change it.
*
* @param string|array $files (Optional) Files to receive
* @return bool
*/
public function receive($files = null)
{
if (!$this->isValid($files)) {
return false;
}
$check = $this->_getFiles($files);
foreach ($check as $file => $content) {
if (!$content['received']) {
$directory = '';
$destination = $this->getDestination($file);
if ($destination !== null) {
$directory = $destination . DIRECTORY_SEPARATOR;
}
/******************************************/
// The original transfer adapter
// passes content['tmp_name']
// but we'll pass in content['name'] instead
// to have access to the extension
/******************************************/
$filename = $directory . $content['name'];
$rename = $this->getFilter('File_Rename');
if ($rename !== null) {
$tmp = $rename->getNewName($content['name']);
if ($tmp != $content['name']) {
$filename = $tmp;
}
if (dirname($filename) == '.') {
$filename = $directory . $filename;
}
$key = array_search(get_class($rename), $this->_files[$file]['filters']);
unset($this->_files[$file]['filters'][$key]);
}
// Should never return false when it's tested by the upload validator
if (!move_uploaded_file($content['tmp_name'], $filename)) {
if ($content['options']['ignoreNoFile']) {
$this->_files[$file]['received'] = true;
$this->_files[$file]['filtered'] = true;
continue;
}
$this->_files[$file]['received'] = false;
return false;
}
if ($rename !== null) {
$this->_files[$file]['destination'] = dirname($filename);
$this->_files[$file]['name'] = basename($filename);
}
$this->_files[$file]['tmp_name'] = $filename;
$this->_files[$file]['received'] = true;
}
if (!$content['filtered']) {
if (!$this->_filter($file)) {
$this->_files[$file]['filtered'] = false;
return false;
}
$this->_files[$file]['filtered'] = true;
}
}
return true;
}
}
That is the adapter, now for the filter.
class My_Filter_File_Rename
extends Zend_Filter_File_Rename
{
/**
* Internal array of array(source, target, overwrite)
*/
protected $_files = array( );
/**
* Class constructor
*
* Options argument may be either a string, a Zend_Config object, or an array.
* If an array or Zend_Config object, it accepts the following keys:
* 'source' => Source filename or directory which will be renamed
* 'target' => Target filename or directory, the new name of the sourcefile
* 'overwrite' => Shall existing files be overwritten ?
* 'keepExtension' => Should the files original extension be kept
*
* @param string|array $options Target file or directory to be renamed
* @param string $target Source filename or directory (deprecated)
* @param bool $overwrite Should existing files be overwritten (deprecated)
* @return void
*/
public function __construct( $options )
{
if( $options instanceof Zend_Config )
{
$options = $options->toArray();
}
elseif( is_string( $options ) )
{
$options = array( 'target' => $options );
}
elseif( !is_array( $options ) )
{
require_once 'Zend/Filter/Exception.php';
throw new Zend_Filter_Exception( 'Invalid options argument provided to filter' );
}
if( 1 setFile( $options );
}
/**
* Returns the files to rename and their new name and location
*
* @return array
*/
public function getFile()
{
return $this->_files;
}
/**
* Sets a new file or directory as target, deleting existing ones
*
* Array accepts the following keys:
* 'source' => Source filename or directory which will be renamed
* 'target' => Target filename or directory, the new name of the sourcefile
* 'overwrite' => Shall existing files be overwritten ?
* 'keepExtension' => Should the files original extension be kept
*
* @param string|array $options Old file or directory to be rewritten
* @return Zend_Filter_File_Rename
*/
public function setFile( $options )
{
$this->_files = array( );
$this->addFile( $options );
return $this;
}
/**
* Adds a new file or directory as target to the existing ones
*
* Array accepts the following keys:
* 'source' => Source filename or directory which will be renamed
* 'target' => Target filename or directory, the new name of the sourcefile
* 'overwrite' => Shall existing files be overwritten ?
* 'keepExtension' => Should the files original extension be kept
*
* @param string|array $options Old file or directory to be rewritten
* @return Zend_Filter_File_Rename
*/
public function addFile( $options )
{
if( is_string( $options ) )
{
$options = array( 'target' => $options );
}
elseif( !is_array( $options ) )
{
require_once 'Zend/Filter/Exception.php';
throw new Zend_Filter_Exception( 'Invalid options to rename filter provided' );
}
$this->_convertOptions( $options );
return $this;
}
/**
* Returns only the new filename without moving it
* But existing files will be erased when the overwrite option is true
*
* @param string $value Full path of file to change
* @param boolean $source Return internal informations
* @return string The new filename which has been set
*/
public function getNewName( $value,
$source = false )
{
$file = $this->_getFileName( $value );
if( $file[ 'source' ] == $file[ 'target' ] )
{
return $value;
}
if( !file_exists( $file[ 'source' ] ) && !$file['keepExtension'] )
{
return $value;
}
if( ($file[ 'overwrite' ] == true) && (file_exists( $file[ 'target' ] )) )
{
unlink( $file[ 'target' ] );
}
if( file_exists( $file[ 'target' ] ) )
{
require_once 'Zend/Filter/Exception.php';
throw new Zend_Filter_Exception( sprintf( "File '%s' could not be renamed. It already exists.",
$value ) );
}
if( $source )
{
return $file;
}
return $file[ 'target' ];
}
/**
* Defined by Zend_Filter_Interface
*
* Renames the file $value to the new name set before
* Returns the file $value, removing all but digit characters
*
* @param string $value Full path of file to change
* @throws Zend_Filter_Exception
* @return string The new filename which has been set, or false when there were errors
*/
public function filter( $value )
{
$file = $this->getNewName( $value, true );
if( is_string( $file ) )
{
return $file;
}
$result = rename( $file[ 'source' ], $file[ 'target' ] );
if( $result === true )
{
return $file[ 'target' ];
}
require_once 'Zend/Filter/Exception.php';
throw new Zend_Filter_Exception( sprintf( "File '%s' could not be renamed. An error occured while processing the file.",
$value ) );
}
/**
* Internal method for creating the file array
* Supports single and nested arrays
*
* @param array $options
* @return array
*/
protected function _convertOptions( $options )
{
$files = array( );
foreach( $options as $key => $value )
{
if( is_array( $value ) )
{
$this->_convertOptions( $value );
continue;
}
switch( $key )
{
case "source":
$files[ 'source' ] = ( string ) $value;
break;
case 'target' :
$files[ 'target' ] = ( string ) $value;
break;
case 'overwrite' :
$files[ 'overwrite' ] = ( boolean ) $value;
break;
case 'keepExtension':
$files[ 'keepExtension' ] = ( boolean ) $value;
break;
default:
break;
}
}
if( empty( $files ) )
{
return $this;
}
if( empty( $files[ 'source' ] ) )
{
$files[ 'source' ] = '*';
}
if( empty( $files[ 'target' ] ) )
{
$files[ 'target' ] = '*';
}
if( empty( $files[ 'overwrite' ] ) )
{
$files[ 'overwrite' ] = false;
}
if( empty( $files[ 'keepExtension' ] ) )
{
$files[ 'keepExtension' ] = true;
}
$found = false;
foreach( $this->_files as $key => $value )
{
if( $value[ 'source' ] == $files[ 'source' ] )
{
$this->_files[ $key ] = $files;
$found = true;
}
}
if( !$found )
{
$count = count( $this->_files );
$this->_files[ $count ] = $files;
}
return $this;
}
/**
* Internal method to resolve the requested source
* and return all other related parameters
*
* @param string $file Filename to get the informations for
* @return array
*/
protected function _getFileName( $file )
{
$rename = array( );
foreach( $this->_files as $value )
{
if( $value[ 'source' ] == '*' )
{
if( !isset( $rename[ 'source' ] ) )
{
$rename = $value;
$rename[ 'source' ] = $file;
}
}
if( $value[ 'source' ] == $file )
{
$rename = $value;
}
}
if( !isset( $rename[ 'source' ] ) )
{
return $file;
}
if( !isset( $rename[ 'target' ] ) or ($rename[ 'target' ] == '*') )
{
$rename[ 'target' ] = $rename[ 'source' ];
}
if( is_dir( $rename[ 'target' ] ) )
{
$name = basename( $rename[ 'source' ] );
$last = $rename[ 'target' ][ strlen( $rename[ 'target' ] ) - 1 ];
if( ($last != '/') and ($last != '\\') )
{
$rename[ 'target' ] .= DIRECTORY_SEPARATOR;
}
$rename[ 'target' ] .= $name;
}
if( !is_dir( $rename['target'] ) || $rename[ 'keepExtension' ] )
{
$name = basename( $rename[ 'source' ] );
$parts = explode( '.', $name );
$extension = $parts[count( $parts ) - 1];
$rename[ 'target' ] .= '.' . $extension;
}
return $rename;
}
}
You'll then have to add the prefix path's to the file element that you've made to upload the file.
$fileElement->addPrefixPath('My_File_Transfer_Adapter', 'My/File/Transfer/Adapter', Zend_Form_Element_File::TRANSFER_ADAPTER );
$fileElement->addPrefixPath( 'My_Filter', 'My/Filter', Zend_Form_Element_File::FILTER );
When you add the filter to the file element you'll have to do it the following way
$fileElement->addFilter(
'File_Rename',
array(
'target' => $this->_getPictureDestination() . DIRECTORY_SEPARATOR . "user$userId",
'overwrite' => true,
'keepExtension' => true
)
)
Now, when the files get moved over to the new directory they'll have the original files extension and they'll have the new name that you specified when you added the filter to the file element.
If this was hard to understand please let me know. It took me a while to figure out what was going on in Zend to do this so if it helps anyone, use this code freely.