diff --git a/src/Storage/Listener/AbstractListener.php b/src/Storage/Listener/AbstractListener.php index 86e74c84..72981b4c 100644 --- a/src/Storage/Listener/AbstractListener.php +++ b/src/Storage/Listener/AbstractListener.php @@ -7,17 +7,20 @@ namespace Burzum\FileStorage\Storage\Listener; use Burzum\FileStorage\Storage\PathBuilder\PathBuilderTrait; +use Burzum\FileStorage\Storage\StorageException; use Burzum\FileStorage\Storage\StorageTrait; use Burzum\FileStorage\Storage\StorageUtils; use Cake\Core\InstanceConfigTrait; use Cake\Datasource\EntityInterface; use Cake\Event\Event; use Cake\Event\EventListenerInterface; +use Cake\Event\EventManager; use Cake\Filesystem\Folder; use Cake\Log\LogTrait; use Cake\ORM\Table; use Cake\Utility\MergeVariablesTrait; use Cake\Utility\Text; +use Psr\Log\LogLevel; /** * AbstractListener @@ -46,30 +49,30 @@ abstract class AbstractListener implements EventListenerInterface { use PathBuilderTrait; use StorageTrait; -/** - * The adapter class - * - * @param null|string - */ + /** + * The adapter class + * + * @param null|string + */ protected $_adapterClass = null; -/** - * List of adapter classes the event listener can work with - * - * It is used in FileStorageEventListenerBase::getAdapterClassName to get the - * class, to detect if an event passed to this listener should be processed or - * not. Only events with an adapter class present in this array will be - * processed. - * - * @var array - */ + /** + * List of adapter classes the event listener can work with + * + * It is used in FileStorageEventListenerBase::getAdapterClassName to get the + * class, to detect if an event passed to this listener should be processed or + * not. Only events with an adapter class present in this array will be + * processed. + * + * @var array + */ protected $_adapterClasses = []; -/** - * Default settings - * - * @var array - */ + /** + * Default settings + * + * @var array + */ protected $_defaultConfig = [ 'pathBuilder' => '', 'pathBuilderOptions' => [], @@ -78,11 +81,11 @@ abstract class AbstractListener implements EventListenerInterface { 'models' => false, ]; -/** - * Constructor - * - * @param array $config - */ + /** + * Constructor + * + * @param array $config + */ public function __construct(array $config = []) { $this->_mergeListenerVars(); $this->config($config); @@ -93,11 +96,11 @@ public function __construct(array $config = []) { $this->initialize($config); } -/** - * Merges properties. - * - * @return void - */ + /** + * Merges properties. + * + * @return void + */ protected function _mergeListenerVars() { $this->_mergeVars( ['_defaultConfig'], @@ -105,29 +108,34 @@ protected function _mergeListenerVars() { ); } -/** - * Helper method to bypass the need to override the constructor. - * - * Called last inside __construct() - * - * @return void - */ + /** + * Helper method to bypass the need to override the constructor. + * + * Called last inside __construct() + * + * @return void + */ public function initialize($config) {} + /** + * implementedEvents + * + * @return array + */ public function implementedEvents() { return [ 'FileStorage.path' => 'getPath' ]; } -/** - * Check if the event is of a type or subject object of type model we want to - * process with this listener. - * - * @throws \InvalidArgumentException - * @param Event $event - * @return boolean - */ + /** + * Check if the event is of a type or subject object of type model we want to + * process with this listener. + * + * @throws \InvalidArgumentException + * @param Event $event + * @return boolean + */ protected function _checkEvent(Event $event) { return ( isset($event->data['table']) @@ -137,12 +145,12 @@ protected function _checkEvent(Event $event) { ); } -/** - * Detects if an entities model field has name of one of the allowed models set. - * - * @param Event $event - * @return boolean - */ + /** + * Detects if an entities model field has name of one of the allowed models set. + * + * @param Event $event + * @return boolean + */ protected function _modelFilter(Event $event) { if (is_array($this->_config['models'])) { $model = $event->data['record']['model']; @@ -153,12 +161,12 @@ protected function _modelFilter(Event $event) { return true; } -/** - * Gets the adapter class name from the adapter config - * - * @param string $configName Name of the configuration - * @return boolean|string False if the config is not present - */ + /** + * Gets the adapter class name from the adapter config + * + * @param string $configName Name of the configuration + * @return boolean|string False if the config is not present + */ protected function _getAdapterClassFromConfig($configName) { $config = $this->storageConfig($configName); if (!empty($config['adapterClass'])) { @@ -167,15 +175,15 @@ protected function _getAdapterClassFromConfig($configName) { return false; } -/** - * Gets the adapter class name from the adapter configuration key and checks if - * it is in the list of supported adapters for the listener. - * - * You must define a list of supported classes via AbstractStorageEventListener::$_adapterClasses. - * - * @param string $configName Name of the adapter configuration. - * @return string|false String, the adapter class name or false if it was not found. - */ + /** + * Gets the adapter class name from the adapter configuration key and checks if + * it is in the list of supported adapters for the listener. + * + * You must define a list of supported classes via AbstractStorageEventListener::$_adapterClasses. + * + * @param string $configName Name of the adapter configuration. + * @return string|false String, the adapter class name or false if it was not found. + */ public function getAdapterClassName($configName) { $className = $this->_getAdapterClassFromConfig($configName); if (in_array($className, $this->_adapterClasses)) { @@ -186,24 +194,24 @@ public function getAdapterClassName($configName) { return false; } -/** - * Create a temporary file locally based on a file from an adapter. - * - * A common case is image manipulation or video processing for example. It is - * required to get the file first from the adapter and then write it to - * a tmp file. Then manipulate it and upload the changed file. - * - * The adapter might not be one that is using a local file system, so we first - * get the file from the storage system, store it locally in a tmp file and - * later load the new file that was generated based on the tmp file into the - * storage adapter. This method here just generates the tmp file. - * - * @param Adapter $Storage Storage adapter - * @param string $path Path / key of the storage adapter file - * @param string $tmpFolder - * @throws \Exception - * @return string - */ + /** + * Create a temporary file locally based on a file from an adapter. + * + * A common case is image manipulation or video processing for example. It is + * required to get the file first from the adapter and then write it to + * a tmp file. Then manipulate it and upload the changed file. + * + * The adapter might not be one that is using a local file system, so we first + * get the file from the storage system, store it locally in a tmp file and + * later load the new file that was generated based on the tmp file into the + * storage adapter. This method here just generates the tmp file. + * + * @param Adapter $Storage Storage adapter + * @param string $path Path / key of the storage adapter file + * @param string $tmpFolder + * @throws \Exception + * @return string + */ protected function _tmpFile($Storage, $path, $tmpFolder = null) { try { $tmpFile = $this->createTmpFile($tmpFolder); @@ -215,33 +223,33 @@ protected function _tmpFile($Storage, $path, $tmpFolder = null) { } } -/** - * Calculates the hash of a file. - * - * You can use this to compare if you got two times the same file uploaded. - * - * @param string $file Path to the file on your local machine. - * @param string $method 'md5' or 'sha1' - * @throws \InvalidArgumentException - * @link http://php.net/manual/en/function.md5-file.php - * @link http://php.net/manual/en/function.sha1-file.php - * @link http://php.net/manual/en/function.sha1-file.php#104748 - * @return string - */ + /** + * Calculates the hash of a file. + * + * You can use this to compare if you got two times the same file uploaded. + * + * @param string $file Path to the file on your local machine. + * @param string $method 'md5' or 'sha1' + * @throws \InvalidArgumentException + * @link http://php.net/manual/en/function.md5-file.php + * @link http://php.net/manual/en/function.sha1-file.php + * @link http://php.net/manual/en/function.sha1-file.php#104748 + * @return string + */ public function calculateFileHash($file, $method = 'sha1') { return StorageUtils::getFileHash($file, $method); } -/** - * Gets the hash for a file storage entity that is going to be stored. - * - * It first checks if hashing is enabled, if it is enabled it uses the the - * configured hashMethod to generate the hash and returns that hash. - * - * @param \Cake\Datasource\EntityInterface - * @param string $fileField - * @return null|string - */ + /** + * Gets the hash for a file storage entity that is going to be stored. + * + * It first checks if hashing is enabled, if it is enabled it uses the the + * configured hashMethod to generate the hash and returns that hash. + * + * @param \Cake\Datasource\EntityInterface + * @param string $fileField + * @return null|string + */ public function getFileHash(EntityInterface $entity, $fileField) { if ($this->config('fileHash') !== false) { return $this->calculateFileHash( @@ -252,18 +260,18 @@ public function getFileHash(EntityInterface $entity, $fileField) { return null; } -/** - * Creates a temporary file name and checks the tmp path, creates one if required. - * - * This method is thought to be used to generate tmp file locations for use cases - * like audio or image process were you need copies of a file and want to avoid - * conflicts. By default the tmp file is generated using cakes TMP constant + - * folder if passed and a uuid as filename. - * - * @param string $folder - * @param boolean $checkAndCreatePath - * @return string For example /var/www/app/tmp/ or /var/www/app/tmp// - */ + /** + * Creates a temporary file name and checks the tmp path, creates one if required. + * + * This method is thought to be used to generate tmp file locations for use cases + * like audio or image process were you need copies of a file and want to avoid + * conflicts. By default the tmp file is generated using cakes TMP constant + + * folder if passed and a uuid as filename. + * + * @param string $folder + * @param boolean $checkAndCreatePath + * @return string For example /var/www/app/tmp/ or /var/www/app/tmp// + */ public function createTmpFile($folder = null, $checkAndCreatePath = true) { if (is_null($folder)) { $folder = TMP; @@ -274,13 +282,122 @@ public function createTmpFile($folder = null, $checkAndCreatePath = true) { return $folder . Text::uuid(); } -/** - * Get the path for a storage entity. - * - * @param \Cake\Event\Event $event - * @return string - */ + /** + * Get the path for a storage entity. + * + * @param \Cake\Event\Event $event + * @return string + */ public function getPath(Event $event) { return $this->pathBuilder()->{$event->data['method']}($event->subject(), $event->data); } + + /** + * Stores the file in the configured storage backend. + * + * @param \Cake\Event\Event $event + * @return bool + * @throws \Burzum\FileStorage\Storage\StorageException + */ + protected function _storeFile(Event $event) { + try { + $this->_handleLegacyEvent($event); + $fileField = $this->config('fileField'); + $entity = $event->data['entity']; + $Storage = $this->storageAdapter($entity['adapter']); + $Storage->write($entity['path'], file_get_contents($entity[$fileField]['tmp_name']), true); + $event->result = $event->data['table']->save($entity, array( + 'checkRules' => false + )); + $this->_afterStoreFile($event); + return true; + } catch (\Exception $e) { + throw $e; + $this->log($e->getMessage(), LogLevel::ERROR, ['scope' => ['storage']]); + throw new StorageException($e->getMessage()); + } + return false; + } + + /** + * Deletes the file from the configured storage backend. + * + * @param \Cake\Event\Event $event + * @return bool + * @throws \Burzum\FileStorage\Storage\StorageException + */ + protected function _deleteFile(Event $event) { + try { + $this->_handleLegacyEvent($event); + $entity = $event->data['entity']; + $path = $this->pathBuilder()->fullPath($entity); + if ($this->storageAdapter($entity->adapter)->delete($path)) { + if ($this->_config['imageProcessing'] === true) { + $this->autoProcessImageVersions($entity, 'remove'); + } + $event->result = true; + $event->data['path'] = $path; + $event->data['entity'] = $entity; + $this->_afterDeleteFile($event); + return true; + } + } catch (\Exception $e) { + $this->log($e->getMessage(), LOG_ERR, ['scope' => ['storage']]); + throw new StorageException($e->getMessage()); + } + return false; + } + + /** + * Callback to handle the case something needs to be done after the file was + * successfully stored in the storage backend. + * + * By default this will trigger an event FileStorage.afterStoreFile but you + * can also just overload this method and implement your own logic here. + * + * This method is a good place to flag a file for some post processing or + * directly doing the post processing like image versions or + * video compression. + * + * @param \Cake\Event\Event $event; + * @return void + */ + protected function _afterStoreFile(Event $event) { + $this->_handleLegacyEvent($event); + $afterStoreEvent = new Event('FileStorage.afterStoreFile', $this, [ + 'entity' => $event->result, + 'adapter' => $this->storageAdapter($event->result['adapter']) + ]); + EventManager::instance()->dispatch($afterStoreEvent); + } + + /** + * Callback to handle the case something needs to be done after the file was + * successfully removed from the storage backend. + * + * @param \Cake\Event\Event $event; + * @return void + */ + protected function _afterDeleteFile(Event $event) { + $this->_handleLegacyEvent($event); + $afterDeleteEvent = new Event('FileStorage.afterDeleteFile', $this, [ + 'entity' => $event->result, + 'adapter' => $this->storageAdapter($event->result['adapter']) + ]); + EventManager::instance()->dispatch($afterDeleteEvent); + } + + /** + * Handles legacy events + * + * - Copies the old 'record' data to 'entity' + * + * @param \Cake\Event\Event + * @return void + */ + protected function _handleLegacyEvent(Event &$event) { + if (isset($event->data['record'])) { + $event->data['entity'] = $event->data['record']; + } + } } diff --git a/src/Storage/Listener/ImageProcessingTrait.php b/src/Storage/Listener/ImageProcessingTrait.php index 24ef374f..72d08426 100644 --- a/src/Storage/Listener/ImageProcessingTrait.php +++ b/src/Storage/Listener/ImageProcessingTrait.php @@ -7,7 +7,6 @@ namespace Burzum\FileStorage\Storage\Listener; use Burzum\FileStorage\Storage\StorageUtils; -use Burzum\Imagine\Lib\ImageProcessor; use Cake\Core\Configure; use Cake\Datasource\EntityInterface; @@ -18,6 +17,7 @@ */ trait ImageProcessingTrait { + protected $_imageProcessorClass = 'Burzum\Imagine\Lib\ImageProcessor'; protected $_imageProcessor = null; protected $_imageVersions = []; protected $_imageVersionHashes = []; @@ -70,7 +70,8 @@ public function imageProcessor(array $config = [], $renew = false) { return $this->_imageProcessor; } $this->_loadImageProcessingFromConfig(); - $this->_imageProcessor = new ImageProcessor($config); + $class = $this->_imageProcessorClass; + $this->_imageProcessor = new $class($config); return $this->_imageProcessor; } diff --git a/src/Storage/Listener/LocalListener.php b/src/Storage/Listener/LocalListener.php index e9ae2e5e..a991e2df 100644 --- a/src/Storage/Listener/LocalListener.php +++ b/src/Storage/Listener/LocalListener.php @@ -81,20 +81,7 @@ public function implementedEvents() { */ public function afterDelete(Event $event, EntityInterface $entity) { if ($this->_checkEvent($event)) { - $path = $this->pathBuilder()->fullPath($entity); - try { - if ($this->storageAdapter($entity->adapter)->delete($path)) { - if ($this->_config['imageProcessing'] === true) { - $this->autoProcessImageVersions($entity, 'remove'); - } - $event->result = true; - return; - } - } catch (\Exception $e) { - $this->log($e->getMessage(), LOG_ERR, ['scope' => ['storage']]); - throw new StorageException($e->getMessage()); - } - $event->result = false; + $event->result = $this->_deleteFile($event);; $event->stopPropagation(); } } @@ -160,29 +147,6 @@ public function imagePath(Event $event) { $event->stopPropagation(); } - /** - * Stores the file in the configured storage backend. - * - * @param \Cake\Event\Event $event - * @throws \Burzum\Filestorage\Storage\StorageException - * @return boolean - */ - protected function _storeFile(Event $event) { - try { - $fileField = $this->config('fileField'); - $entity = $event->data['record']; - $Storage = $this->storageAdapter($entity['adapter']); - $Storage->write($entity['path'], file_get_contents($entity[$fileField]['tmp_name']), true); - $event->result = $event->data['table']->save($entity, array( - 'checkRules' => false - )); - return true; - } catch (\Exception $e) { - $this->log($e->getMessage(), LogLevel::ERROR, ['scope' => ['storage']]); - throw new StorageException($e->getMessage()); - } - } - /** * Removes a specific image version. * diff --git a/tests/TestCase/Storage/Listener/LocalListenerTest.php b/tests/TestCase/Storage/Listener/LocalListenerTest.php index 9a5186d0..10205467 100644 --- a/tests/TestCase/Storage/Listener/LocalListenerTest.php +++ b/tests/TestCase/Storage/Listener/LocalListenerTest.php @@ -135,6 +135,7 @@ public function testAfterDelete() { $entity = $this->FileStorage->get('file-storage-3'); $event = new Event('FileStorage.afterDelete', $this->FileStorage, [ 'record' => $entity, + 'entity' => $entity, 'table' => $this->FileStorage ]);