From a37ffe28b5dacbaf9b55da588e183c177f421abd Mon Sep 17 00:00:00 2001 From: mscherer Date: Fri, 6 Nov 2020 01:46:52 +0100 Subject: [PATCH 01/17] Add width/height --- src/FileStorage/DataTransformer.php | 2 +- src/Model/Behavior/FileStorageBehavior.php | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/FileStorage/DataTransformer.php b/src/FileStorage/DataTransformer.php index 943de9c..d19c018 100644 --- a/src/FileStorage/DataTransformer.php +++ b/src/FileStorage/DataTransformer.php @@ -73,7 +73,7 @@ public function entityToFileObject(EntityInterface $entity): FileInterface public function fileObjectToEntity(FileInterface $file, ?EntityInterface $entity): EntityInterface { $data = [ - 'id' => $file->uuid(), + 'id' => $file->uuid(), //FIXME 'model' => $file->model(), 'foreign_key' => $file->modelId(), 'filesize' => $file->filesize(), diff --git a/src/Model/Behavior/FileStorageBehavior.php b/src/Model/Behavior/FileStorageBehavior.php index 4f95446..4a3cc0d 100644 --- a/src/Model/Behavior/FileStorageBehavior.php +++ b/src/Model/Behavior/FileStorageBehavior.php @@ -36,9 +36,9 @@ class FileStorageBehavior extends Behavior protected FileStorage $fileStorage; /** - * @var \Phauthentic\Infrastructure\Storage\Processor\ProcessorInterface + * @var \Phauthentic\Infrastructure\Storage\Processor\ProcessorInterface|null */ - protected ?ProcessorInterface $imageProcessor; + protected $imageProcessor; /** * @var \Burzum\FileStorage\FileStorage\DataTransformerInterface @@ -219,7 +219,7 @@ protected function checkEntityBeforeSave(EntityInterface $entity) { if ($entity->isNew()) { if (!$entity->has('model')) { - $entity->set('model', $this->getTable()->getTable()); + $entity->set('model', $this->getTable()->getAlias()); } if (!$entity->has('adapter')) { @@ -338,12 +338,15 @@ public function processImages(FileInterface $file, EntityInterface $entity): Fil $model = $file->model(); $identifier = $entity->get('identifier'); + $model = 'Events'; //FIXME + $identifier = 'EventImages'; //FIXME + if (!isset($imageSizes[$model][$identifier])) { return $file; } $file = $file->withVariants($imageSizes[$model][$identifier]); - $file = $this->imageProcessor->process($file); + $file = $this->getImageProcessor()->process($file); return $file; } From 10e1fe0f21647c7c141b2cfe6d6c93dab31a1f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Wed, 11 Nov 2020 00:16:49 +0100 Subject: [PATCH 02/17] Adding the FileAssociationBehavior.php --- .../Behavior/FileAssociationBehavior.php | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/Model/Behavior/FileAssociationBehavior.php diff --git a/src/Model/Behavior/FileAssociationBehavior.php b/src/Model/Behavior/FileAssociationBehavior.php new file mode 100644 index 0000000..995aeef --- /dev/null +++ b/src/Model/Behavior/FileAssociationBehavior.php @@ -0,0 +1,114 @@ + [] + ]; + + /** + * @inheritdoc + */ + public function initialize(array $config): void + { + $class = get_class($this->getTable()); + foreach ($config['associations'] as $association => &$assocConfig) { + $defaults = [ + 'overrideable' => false, + 'model' => substr($class, strrpos($class, '\\') + 1), + 'property' => $this->getTable()->getAssociation($association)->getProperty() + ]; + + $assoConfig += $assoConfig; + } + + parent::initialize($config); + } + + /** + * @param \Cake\Event\EventInterface $event + * @param \App\Model\Entity\Event $entity + * @param \ArrayObject $options + * + * @return void + */ + public function afterSave( + EventInterface $event, + EntityInterface $entity, + ArrayObject $options + ): void { + $associations = $this->getConfig('assocations'); + + foreach ($associations as $association => $assocConfig) { + $property = $assocConfig['property']; + if ($entity->{$property} === null) { + continue; + } + + if ($entity->id && $entity->{$property} && $entity->{$property}->file->getError() === UPLOAD_ERR_OK) { + if ($assocConfig['overrideable'] === true) { + $this->findAndRemovePreviousFile($entity, $association, $assocConfig); + } + + $entity->{$property}->set('collection', $assocConfig['collection']); + $entity->{$property}->set('model', $assocConfig['model']); + $entity->{$property}->set('foreign_key', $entity->id); + + $this->{$association}->saveOrFail($entity->{$property}); + } + } + } + + /** + * @param \Cake\Event\EventInterface $event + * @param string $association + * @param array $assocConfig + * @return void + */ + protected function findAndRemovePreviousFile( + EntityInterface $entity, + string $association, + array $assocConfig + ): void { + $result = $this->{$association}->find() + ->where([ + 'collection' => $assocConfig['collection'], + 'model' => $assocConfig['model'], + 'foreign_key' => $entity->get((string)$this->getTable()->getPrimaryKey()) + ]); + + if ($result) { + $this->{$association}->delete($result); + } + } +} From 7f5da42966edb7c55328595877a37afc5a038805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Wed, 11 Nov 2020 01:24:13 +0100 Subject: [PATCH 03/17] Refactoring information gathering for uploaded records --- .../20201110234846_AddCollectionColumn.php | 21 +++++++++++++ .../Behavior/FileAssociationBehavior.php | 30 +++++++++++-------- src/Model/Behavior/FileStorageBehavior.php | 9 ++---- tests/Fixture/FileStorageFixture.php | 3 +- 4 files changed, 43 insertions(+), 20 deletions(-) create mode 100644 config/Migrations/20201110234846_AddCollectionColumn.php diff --git a/config/Migrations/20201110234846_AddCollectionColumn.php b/config/Migrations/20201110234846_AddCollectionColumn.php new file mode 100644 index 0000000..2edca26 --- /dev/null +++ b/config/Migrations/20201110234846_AddCollectionColumn.php @@ -0,0 +1,21 @@ +table('file_storage') + ->addColumn('collection', 'string', ['length' => 128, 'null' => true, 'default' => null]) + ->update(); + } +} diff --git a/src/Model/Behavior/FileAssociationBehavior.php b/src/Model/Behavior/FileAssociationBehavior.php index 995aeef..ef8afa0 100644 --- a/src/Model/Behavior/FileAssociationBehavior.php +++ b/src/Model/Behavior/FileAssociationBehavior.php @@ -12,6 +12,7 @@ use Cake\Datasource\EntityInterface; use Cake\Event\EventDispatcherTrait; use Cake\Event\EventInterface; +use Cake\ORM\Association\HasOne; use Cake\ORM\Behavior; use League\Flysystem\AdapterInterface; use Phauthentic\Infrastructure\Storage\FileInterface; @@ -41,18 +42,22 @@ class FileAssociationBehavior extends Behavior */ public function initialize(array $config): void { + parent::initialize($config); + $class = get_class($this->getTable()); - foreach ($config['associations'] as $association => &$assocConfig) { + foreach ($config['associations'] as $association => $assocConfig) { + $associationObject = $this->getTable()->getAssociation($association); + $defaults = [ - 'overrideable' => false, - 'model' => substr($class, strrpos($class, '\\') + 1), + 'replace' => $associationObject instanceof HasOne, + 'model' => substr($class, strrpos($class, '\\') + 1, -5), 'property' => $this->getTable()->getAssociation($association)->getProperty() ]; - $assoConfig += $assoConfig; + $config['associations'][$association] = $assocConfig += $defaults; } - parent::initialize($config); + $this->setConfig('associations', $config['associations']); } /** @@ -62,12 +67,12 @@ public function initialize(array $config): void * * @return void */ - public function afterSave( + public function beforeSave( EventInterface $event, EntityInterface $entity, ArrayObject $options ): void { - $associations = $this->getConfig('assocations'); + $associations = $this->getConfig('associations'); foreach ($associations as $association => $assocConfig) { $property = $assocConfig['property']; @@ -76,15 +81,13 @@ public function afterSave( } if ($entity->id && $entity->{$property} && $entity->{$property}->file->getError() === UPLOAD_ERR_OK) { - if ($assocConfig['overrideable'] === true) { + if ($assocConfig['replace'] === true) { $this->findAndRemovePreviousFile($entity, $association, $assocConfig); } $entity->{$property}->set('collection', $assocConfig['collection']); $entity->{$property}->set('model', $assocConfig['model']); $entity->{$property}->set('foreign_key', $entity->id); - - $this->{$association}->saveOrFail($entity->{$property}); } } } @@ -100,15 +103,16 @@ protected function findAndRemovePreviousFile( string $association, array $assocConfig ): void { - $result = $this->{$association}->find() + $result = $this->getTable()->{$association}->find() ->where([ 'collection' => $assocConfig['collection'], 'model' => $assocConfig['model'], 'foreign_key' => $entity->get((string)$this->getTable()->getPrimaryKey()) - ]); + ]) + ->first(); if ($result) { - $this->{$association}->delete($result); + $this->getTable()->{$association}->delete($result); } } } diff --git a/src/Model/Behavior/FileStorageBehavior.php b/src/Model/Behavior/FileStorageBehavior.php index 4a3cc0d..829d1cf 100644 --- a/src/Model/Behavior/FileStorageBehavior.php +++ b/src/Model/Behavior/FileStorageBehavior.php @@ -336,16 +336,13 @@ public function processImages(FileInterface $file, EntityInterface $entity): Fil { $imageSizes = Configure::read('FileStorage.imageVariants'); $model = $file->model(); - $identifier = $entity->get('identifier'); + $collection = $entity->get('collection'); - $model = 'Events'; //FIXME - $identifier = 'EventImages'; //FIXME - - if (!isset($imageSizes[$model][$identifier])) { + if (!isset($imageSizes[$model][$collection])) { return $file; } - $file = $file->withVariants($imageSizes[$model][$identifier]); + $file = $file->withVariants($imageSizes[$model][$collection]); $file = $this->getImageProcessor()->process($file); return $file; diff --git a/tests/Fixture/FileStorageFixture.php b/tests/Fixture/FileStorageFixture.php index b72d5a6..60b5d6e 100644 --- a/tests/Fixture/FileStorageFixture.php +++ b/tests/Fixture/FileStorageFixture.php @@ -32,13 +32,14 @@ class FileStorageFixture extends TestFixture 'user_id' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 36], 'foreign_key' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 36], 'model' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 64], + 'collection' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 128], 'filename' => ['type' => 'string', 'null' => false, 'default' => null], 'filesize' => ['type' => 'integer', 'null' => true, 'default' => null, 'length' => 16], 'mime_type' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 32], 'extension' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 32], 'hash' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 64], 'path' => ['type' => 'string', 'null' => true, 'default' => null], - 'adapter' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 32, 'comment' => 'Gaufrette Storage Adapter Class'], + 'adapter' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 32], 'variants' => ['type' => 'json', 'null' => true, 'default' => null], 'metadata' => ['type' => 'json', 'null' => true, 'default' => null], 'created' => ['type' => 'datetime', 'null' => true, 'default' => null], From 4f2f186504e0b72c2ab332cd7ac468fcbfa251ec Mon Sep 17 00:00:00 2001 From: mscherer Date: Thu, 12 Nov 2020 02:38:34 +0100 Subject: [PATCH 04/17] Move image to file processing stack --- .../Behavior/FileAssociationBehavior.php | 5 +- src/Model/Behavior/FileStorageBehavior.php | 47 +++++++++---------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/Model/Behavior/FileAssociationBehavior.php b/src/Model/Behavior/FileAssociationBehavior.php index ef8afa0..30a3dc9 100644 --- a/src/Model/Behavior/FileAssociationBehavior.php +++ b/src/Model/Behavior/FileAssociationBehavior.php @@ -82,7 +82,7 @@ public function beforeSave( if ($entity->id && $entity->{$property} && $entity->{$property}->file->getError() === UPLOAD_ERR_OK) { if ($assocConfig['replace'] === true) { - $this->findAndRemovePreviousFile($entity, $association, $assocConfig); + //$this->findAndRemovePreviousFile($entity, $association, $assocConfig); } $entity->{$property}->set('collection', $assocConfig['collection']); @@ -107,7 +107,8 @@ protected function findAndRemovePreviousFile( ->where([ 'collection' => $assocConfig['collection'], 'model' => $assocConfig['model'], - 'foreign_key' => $entity->get((string)$this->getTable()->getPrimaryKey()) + 'foreign_key' => $entity->get((string)$this->getTable()->getPrimaryKey()), + 'id !=' => $entity->get($assocConfig['property'])->get($this->getTable()->{$association}->getPrimaryKey()) ]) ->first(); diff --git a/src/Model/Behavior/FileStorageBehavior.php b/src/Model/Behavior/FileStorageBehavior.php index 829d1cf..1ba9bf3 100644 --- a/src/Model/Behavior/FileStorageBehavior.php +++ b/src/Model/Behavior/FileStorageBehavior.php @@ -33,17 +33,17 @@ class FileStorageBehavior extends Behavior /** * @var \Phauthentic\Infrastructure\Storage\FileStorage */ - protected FileStorage $fileStorage; + protected $fileStorage; /** - * @var \Phauthentic\Infrastructure\Storage\Processor\ProcessorInterface|null + * @var \Burzum\FileStorage\FileStorage\DataTransformerInterface */ - protected $imageProcessor; + protected $transformer; /** - * @var \Burzum\FileStorage\FileStorage\DataTransformerInterface + * @var \Phauthentic\Infrastructure\Storage\Processor\ProcessorInterface */ - protected DataTransformerInterface $transformer; + protected $processor; /** * Default config @@ -55,14 +55,9 @@ class FileStorageBehavior extends Behavior 'ignoreEmptyFile' => true, 'fileField' => 'file', 'fileStorage' => null, - 'imageProcessor' => null, + 'fileProcessor' => null, ]; - /** - * @var array - */ - protected array $processors = []; - /** * @inheritDoc * @@ -85,6 +80,8 @@ public function initialize(array $config): void $this->getTable() ); } + + $this->processors = (array)$this->getConfig('processors'); } /** @@ -184,11 +181,12 @@ public function afterSave(EventInterface $event, EntityInterface $entity, ArrayO try { $file = $this->entityToFileObject($entity); $file = $this->fileStorage->store($file); + + // TODO: move into stack processing $file = $this->processImages($file, $entity); - foreach ($this->processors as $processor) { - $file = $processor->process($file); - } + $processor = $this->getFileProcessor(); + $file = $processor->process($file); $entity = $this->fileObjectToEntity($file, $entity); $this->getTable()->save( @@ -285,7 +283,7 @@ protected function getFileInfoFromUpload(&$upload, $field = 'file') * * @return int Number of deleted records / files */ - public function deleteAllFiles($conditions) + public function deleteAllFiles(array $conditions) { $table = $this->getTable(); @@ -334,7 +332,7 @@ public function fileObjectToEntity(FileInterface $file, ?EntityInterface $entity */ public function processImages(FileInterface $file, EntityInterface $entity): FileInterface { - $imageSizes = Configure::read('FileStorage.imageVariants'); + $imageSizes = (array)Configure::read('FileStorage.imageVariants'); $model = $file->model(); $collection = $entity->get('collection'); @@ -343,7 +341,6 @@ public function processImages(FileInterface $file, EntityInterface $entity): Fil } $file = $file->withVariants($imageSizes[$model][$collection]); - $file = $this->getImageProcessor()->process($file); return $file; } @@ -353,20 +350,20 @@ public function processImages(FileInterface $file, EntityInterface $entity): Fil * * @return \Phauthentic\Infrastructure\Storage\Processor\ProcessorInterface */ - protected function getImageProcessor(): ProcessorInterface + protected function getFileProcessor(): ProcessorInterface { - if ($this->imageProcessor !== null) { - return $this->imageProcessor; + if ($this->processor !== null) { + return $this->processor; } - if ($this->getConfig('imageProcessor') instanceof ProcessorInterface) { - $this->imageProcessor = $this->getConfig('imageProcessor'); + if ($this->getConfig('fileProcessor') instanceof ProcessorInterface) { + $this->processor = $this->getConfig('fileProcessor'); } - if ($this->imageProcessor === null) { - throw new RuntimeException('No image processor found'); + if ($this->processor === null) { + throw new RuntimeException('No processor found'); } - return $this->imageProcessor; + return $this->processor; } } From 5ecb05b7231fe19bca61dbe7a829a265a0465050 Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 15 Nov 2020 17:27:15 +0100 Subject: [PATCH 05/17] Fix upload for edit mode. --- phpcs.xml | 4 +- phpstan.neon | 4 +- .../Behavior/FileAssociationBehavior.php | 51 +++++++++++-------- src/Model/Behavior/FileStorageBehavior.php | 4 +- src/Model/Entity/FileStorage.php | 18 +++++++ src/Model/Table/FileStorageTable.php | 16 ++++++ src/Shell/ImageVersionShell.php | 5 +- src/View/Helper/ImageHelper.php | 2 +- 8 files changed, 75 insertions(+), 29 deletions(-) diff --git a/phpcs.xml b/phpcs.xml index 260755e..61f0a9a 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -10,12 +10,12 @@ - + - + diff --git a/phpstan.neon b/phpstan.neon index bd7887a..02e6eb7 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ parameters: - level: 7 + level: 8 checkMissingIterableValueType: false checkGenericClassInNonGenericObjectType: false bootstrapFiles: @@ -7,3 +7,5 @@ parameters: earlyTerminatingMethodCalls: Cake\Console\Shell: - abort + ignoreErrors: + - '#Cannot cast array\\|string to string#' diff --git a/src/Model/Behavior/FileAssociationBehavior.php b/src/Model/Behavior/FileAssociationBehavior.php index 30a3dc9..aaf7531 100644 --- a/src/Model/Behavior/FileAssociationBehavior.php +++ b/src/Model/Behavior/FileAssociationBehavior.php @@ -4,22 +4,12 @@ namespace Burzum\FileStorage\Model\Behavior; -use App\Storage\Identifiers; use ArrayObject; -use Burzum\FileStorage\FileStorage\DataTransformer; -use Burzum\FileStorage\FileStorage\DataTransformerInterface; -use Cake\Core\Configure; use Cake\Datasource\EntityInterface; -use Cake\Event\EventDispatcherTrait; use Cake\Event\EventInterface; use Cake\ORM\Association\HasOne; use Cake\ORM\Behavior; -use League\Flysystem\AdapterInterface; -use Phauthentic\Infrastructure\Storage\FileInterface; -use Phauthentic\Infrastructure\Storage\FileStorage; -use Phauthentic\Infrastructure\Storage\Processor\ProcessorInterface; -use RuntimeException; -use Throwable; +use Laminas\Diactoros\UploadedFile; /** * File Association Behavior. @@ -31,14 +21,15 @@ class FileAssociationBehavior extends Behavior { /** - * @inheritdoc + * @var array + * @inheritDoc */ protected $_defaultConfig = [ - 'associations' => [] + 'associations' => [], ]; /** - * @inheritdoc + * @inheritDoc */ public function initialize(array $config): void { @@ -51,10 +42,10 @@ public function initialize(array $config): void $defaults = [ 'replace' => $associationObject instanceof HasOne, 'model' => substr($class, strrpos($class, '\\') + 1, -5), - 'property' => $this->getTable()->getAssociation($association)->getProperty() + 'property' => $this->getTable()->getAssociation($association)->getProperty(), ]; - $config['associations'][$association] = $assocConfig += $defaults; + $config['associations'][$association] = $assocConfig + $defaults; } $this->setConfig('associations', $config['associations']); @@ -62,12 +53,12 @@ public function initialize(array $config): void /** * @param \Cake\Event\EventInterface $event - * @param \App\Model\Entity\Event $entity + * @param \Cake\Datasource\EntityInterface $entity * @param \ArrayObject $options * * @return void */ - public function beforeSave( + public function afterSave( EventInterface $event, EntityInterface $entity, ArrayObject $options @@ -80,22 +71,38 @@ public function beforeSave( continue; } - if ($entity->id && $entity->{$property} && $entity->{$property}->file->getError() === UPLOAD_ERR_OK) { + if ($entity->id && $entity->{$property} && $entity->{$property}->file) { + $file = $entity->{$property}->file; + + $ok = false; + if (is_array($file) && $file['error'] === UPLOAD_ERR_OK) { + $ok = true; + } elseif ($file instanceof UploadedFile && $file->getError() === UPLOAD_ERR_OK) { + $ok = true; + } + + if (!$ok) { + continue; + } + if ($assocConfig['replace'] === true) { - //$this->findAndRemovePreviousFile($entity, $association, $assocConfig); + $this->findAndRemovePreviousFile($entity, $association, $assocConfig); } $entity->{$property}->set('collection', $assocConfig['collection']); $entity->{$property}->set('model', $assocConfig['model']); $entity->{$property}->set('foreign_key', $entity->id); + + $this->getTable()->{$association}->saveOrFail($entity->{$property}); } } } /** - * @param \Cake\Event\EventInterface $event + * @param \Cake\Datasource\EntityInterface $entity * @param string $association * @param array $assocConfig + * * @return void */ protected function findAndRemovePreviousFile( @@ -108,7 +115,7 @@ protected function findAndRemovePreviousFile( 'collection' => $assocConfig['collection'], 'model' => $assocConfig['model'], 'foreign_key' => $entity->get((string)$this->getTable()->getPrimaryKey()), - 'id !=' => $entity->get($assocConfig['property'])->get($this->getTable()->{$association}->getPrimaryKey()) + 'id !=' => $entity->get($assocConfig['property'])->get((string)$this->getTable()->{$association}->getPrimaryKey()), ]) ->first(); diff --git a/src/Model/Behavior/FileStorageBehavior.php b/src/Model/Behavior/FileStorageBehavior.php index 1ba9bf3..190fb93 100644 --- a/src/Model/Behavior/FileStorageBehavior.php +++ b/src/Model/Behavior/FileStorageBehavior.php @@ -81,7 +81,7 @@ public function initialize(array $config): void ); } - $this->processors = (array)$this->getConfig('processors'); + //$this->processors = (array)$this->getConfig('processors'); } /** @@ -264,7 +264,7 @@ protected function getFileInfoFromUpload(&$upload, $field = 'file') if (!is_array($uploadedFile)) { $upload['filesize'] = $uploadedFile->getSize(); $upload['mime_type'] = $uploadedFile->getClientMediaType(); - $upload['extension'] = pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION); + $upload['extension'] = pathinfo((string)$uploadedFile->getClientFilename(), PATHINFO_EXTENSION); $upload['filename'] = $uploadedFile->getClientFilename(); } else { $upload['filesize'] = $uploadedFile['size']; diff --git a/src/Model/Entity/FileStorage.php b/src/Model/Entity/FileStorage.php index 4913d8a..1b063c0 100644 --- a/src/Model/Entity/FileStorage.php +++ b/src/Model/Entity/FileStorage.php @@ -12,6 +12,24 @@ * @author Florian Krämer * @copyright 2012 - 2020 Florian Krämer * @license MIT + * + * @property array $variants + * @property array $metadata + * @property int $id + * @property int|null $user_id + * @property int|null $foreign_key + * @property string|null $model + * @property string|null $filename + * @property int|null $filesize + * @property string|null $mime_type + * @property string|null $extension + * @property string|null $hash + * @property string|null $path + * @property string|null $adapter + * @property \Cake\I18n\FrozenTime $created + * @property \Cake\I18n\FrozenTime $modified + * @property string|null $collection + * @property array $variant_urls */ class FileStorage extends Entity implements FileStorageEntityInterface { diff --git a/src/Model/Table/FileStorageTable.php b/src/Model/Table/FileStorageTable.php index 9aa7ced..95e357b 100644 --- a/src/Model/Table/FileStorageTable.php +++ b/src/Model/Table/FileStorageTable.php @@ -25,6 +25,22 @@ * @author Florian Krämer * @copyright 2012 - 2020 Florian Krämer * @license MIT + * + * @method \Burzum\FileStorage\Model\Entity\FileStorage newEmptyEntity() + * @method \Burzum\FileStorage\Model\Entity\FileStorage newEntity(array $data, array $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage[] newEntities(array $data, array $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage get($primaryKey, $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage findOrCreate($search, ?callable $callback = null, $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage[] patchEntities(iterable $entities, array $data, array $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage|false save(\Cake\Datasource\EntityInterface $entity, $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage saveOrFail(\Cake\Datasource\EntityInterface $entity, $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage[]|\Cake\Datasource\ResultSetInterface|false saveMany(iterable $entities, $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage[]|\Cake\Datasource\ResultSetInterface saveManyOrFail(iterable $entities, $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage[]|\Cake\Datasource\ResultSetInterface|false deleteMany(iterable $entities, $options = []) + * @method \Burzum\FileStorage\Model\Entity\FileStorage[]|\Cake\Datasource\ResultSetInterface deleteManyOrFail(iterable $entities, $options = []) + * @mixin \Cake\ORM\Behavior\TimestampBehavior + * @mixin \Burzum\FileStorage\Model\Behavior\FileStorageBehavior */ class FileStorageTable extends Table { diff --git a/src/Shell/ImageVersionShell.php b/src/Shell/ImageVersionShell.php index 53fbd6f..2f6a0eb 100644 --- a/src/Shell/ImageVersionShell.php +++ b/src/Shell/ImageVersionShell.php @@ -25,7 +25,7 @@ class ImageVersionShell extends Shell /** * Storage Table Object * - * @var \Cake\ORM\Table|null + * @var \Cake\ORM\Table */ public $Table; @@ -200,6 +200,9 @@ public function regenerate(): void foreach ($operations as $version => $operation) { try { + if ($this->command === null) { + $this->abort('No command given'); + } $this->_loop($this->command, $this->args[0], [$version => $operation], $options); } catch (Exception $e) { $this->abort($e->getMessage()); diff --git a/src/View/Helper/ImageHelper.php b/src/View/Helper/ImageHelper.php index e1f36f5..0ac05d6 100644 --- a/src/View/Helper/ImageHelper.php +++ b/src/View/Helper/ImageHelper.php @@ -88,7 +88,7 @@ public function imageUrl(FileStorageEntityInterface $image, ?string $variant = n } if (!$path) { - throw VariantDoesNotExistException::withName($variant); + throw VariantDoesNotExistException::withName((string)$variant); } $options = array_merge($this->getConfig(), $options); From 06f108e87cf398a775407f856c59bb37f607b7f1 Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 22 Nov 2020 13:50:00 +0100 Subject: [PATCH 06/17] Do not expost id --- src/Model/Entity/FileStorage.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Model/Entity/FileStorage.php b/src/Model/Entity/FileStorage.php index 1b063c0..c4da85d 100644 --- a/src/Model/Entity/FileStorage.php +++ b/src/Model/Entity/FileStorage.php @@ -40,6 +40,7 @@ class FileStorage extends Entity implements FileStorageEntityInterface */ protected $_accessible = [ '*' => true, + 'id' => false, ]; /** From ceffdbf87ae4714b3d241e11a85b338ee340d7c7 Mon Sep 17 00:00:00 2001 From: mscherer Date: Mon, 23 Nov 2020 20:49:30 +0100 Subject: [PATCH 07/17] Use saveOrFail() --- src/Model/Behavior/FileStorageBehavior.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Model/Behavior/FileStorageBehavior.php b/src/Model/Behavior/FileStorageBehavior.php index 190fb93..271df93 100644 --- a/src/Model/Behavior/FileStorageBehavior.php +++ b/src/Model/Behavior/FileStorageBehavior.php @@ -189,7 +189,7 @@ public function afterSave(EventInterface $event, EntityInterface $entity, ArrayO $file = $processor->process($file); $entity = $this->fileObjectToEntity($file, $entity); - $this->getTable()->save( + $this->getTable()->saveOrFail( $entity, ['callbacks' => false] ); From 446c05564443061405ffc3819a412323334291dc Mon Sep 17 00:00:00 2001 From: mscherer Date: Tue, 24 Nov 2020 01:39:57 +0100 Subject: [PATCH 08/17] Use beforeSave callback for collection setting. --- .../Behavior/FileAssociationBehavior.php | 24 ++++ tests/Fixture/FileStorageFixture.php | 26 ++-- tests/Fixture/ItemFixture.php | 5 +- tests/Fixture/UuidFileStorageFixture.php | 127 ++++++++++++++++++ tests/Fixture/UuidItemFixture.php | 59 ++++++++ 5 files changed, 222 insertions(+), 19 deletions(-) create mode 100644 tests/Fixture/UuidFileStorageFixture.php create mode 100644 tests/Fixture/UuidItemFixture.php diff --git a/src/Model/Behavior/FileAssociationBehavior.php b/src/Model/Behavior/FileAssociationBehavior.php index aaf7531..e00f66b 100644 --- a/src/Model/Behavior/FileAssociationBehavior.php +++ b/src/Model/Behavior/FileAssociationBehavior.php @@ -51,6 +51,30 @@ public function initialize(array $config): void $this->setConfig('associations', $config['associations']); } + /** + * @param \Cake\Event\EventInterface $event + * @param \Cake\Datasource\EntityInterface $entity + * @param \ArrayObject $options + * + * @return void + */ + public function beforeSave( + EventInterface $event, + EntityInterface $entity, + ArrayObject $options + ): void + { + $associations = $this->getConfig('associations'); + foreach ($associations as $association => $assocConfig) { + $property = $assocConfig['property']; + if ($entity->{$property} === null) { + continue; + } + + $entity->{$property}->set('collection', $assocConfig['collection']); + } + } + /** * @param \Cake\Event\EventInterface $event * @param \Cake\Datasource\EntityInterface $entity diff --git a/tests/Fixture/FileStorageFixture.php b/tests/Fixture/FileStorageFixture.php index 60b5d6e..58cffdd 100644 --- a/tests/Fixture/FileStorageFixture.php +++ b/tests/Fixture/FileStorageFixture.php @@ -28,9 +28,9 @@ class FileStorageFixture extends TestFixture * @var array */ public $fields = [ - 'id' => ['type' => 'uuid', 'null' => true, 'default' => null, 'length' => 36], - 'user_id' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 36], - 'foreign_key' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 36], + 'id' => ['type' => 'integer', 'null' => true, 'default' => null], + 'user_id' => ['type' => 'integer', 'null' => true, 'default' => null], + 'foreign_key' => ['type' => 'integer', 'null' => true, 'default' => null], 'model' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 64], 'collection' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 128], 'filename' => ['type' => 'string', 'null' => false, 'default' => null], @@ -56,9 +56,8 @@ class FileStorageFixture extends TestFixture */ public $records = [ [ - 'id' => 'file-storage-1', - 'user_id' => 'user-1', - 'foreign_key' => 'item-1', + 'user_id' => 1, + 'foreign_key' => 1, 'model' => 'Item', 'filename' => 'cake.icon.png', 'filesize' => '', @@ -73,9 +72,8 @@ class FileStorageFixture extends TestFixture 'modified' => '2012-01-01 12:00:00', ], [ - 'id' => 'file-storage-2', - 'user_id' => 'user-1', - 'foreign_key' => 'item-1', + 'user_id' => 1, + 'foreign_key' => 1, 'model' => 'Item', 'filename' => 'titus-bienebek-bridle.jpg', 'filesize' => '', @@ -90,9 +88,8 @@ class FileStorageFixture extends TestFixture 'modified' => '2012-01-01 12:00:00', ], [ - 'id' => 'file-storage-3', - 'user_id' => 'user-1', - 'foreign_key' => 'item-2', + 'user_id' => 1, + 'foreign_key' => 2, 'model' => 'Item', 'filename' => 'titus.jpg', 'filesize' => '335872', @@ -107,9 +104,8 @@ class FileStorageFixture extends TestFixture 'modified' => '2012-01-01 12:00:00', ], [ - 'id' => 'file-storage-4', - 'user_id' => 'user-1', - 'foreign_key' => 'item-4', + 'user_id' => 1, + 'foreign_key' => 4, 'model' => 'Item', 'filename' => 'titus.jpg', 'filesize' => '335872', diff --git a/tests/Fixture/ItemFixture.php b/tests/Fixture/ItemFixture.php index af3e09c..7fe45e9 100644 --- a/tests/Fixture/ItemFixture.php +++ b/tests/Fixture/ItemFixture.php @@ -28,7 +28,7 @@ class ItemFixture extends TestFixture * @var array */ public $fields = [ - 'id' => ['type' => 'uuid', 'null' => true, 'default' => null, 'length' => 36], + 'id' => ['type' => 'integer', 'null' => true, 'default' => null], 'name' => ['type' => 'string', 'null' => true, 'default' => null], 'path' => ['type' => 'string', 'null' => true, 'default' => null], 'filename' => ['type' => 'string', 'null' => true, 'default' => null], @@ -44,15 +44,12 @@ class ItemFixture extends TestFixture */ public $records = [ [ - 'id' => 'item-1', 'name' => 'Cake', ], [ - 'id' => 'item-2', 'name' => 'More Cake', ], [ - 'id' => 'item-3', 'name' => 'A lot Cake', ], ]; diff --git a/tests/Fixture/UuidFileStorageFixture.php b/tests/Fixture/UuidFileStorageFixture.php new file mode 100644 index 0000000..60b5d6e --- /dev/null +++ b/tests/Fixture/UuidFileStorageFixture.php @@ -0,0 +1,127 @@ + ['type' => 'uuid', 'null' => true, 'default' => null, 'length' => 36], + 'user_id' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 36], + 'foreign_key' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 36], + 'model' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 64], + 'collection' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 128], + 'filename' => ['type' => 'string', 'null' => false, 'default' => null], + 'filesize' => ['type' => 'integer', 'null' => true, 'default' => null, 'length' => 16], + 'mime_type' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 32], + 'extension' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 32], + 'hash' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 64], + 'path' => ['type' => 'string', 'null' => true, 'default' => null], + 'adapter' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 32], + 'variants' => ['type' => 'json', 'null' => true, 'default' => null], + 'metadata' => ['type' => 'json', 'null' => true, 'default' => null], + 'created' => ['type' => 'datetime', 'null' => true, 'default' => null], + 'modified' => ['type' => 'datetime', 'null' => true, 'default' => null], + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id']], + ], + ]; + + /** + * Records + * + * @var array + */ + public $records = [ + [ + 'id' => 'file-storage-1', + 'user_id' => 'user-1', + 'foreign_key' => 'item-1', + 'model' => 'Item', + 'filename' => 'cake.icon.png', + 'filesize' => '', + 'mime_type' => 'image/png', + 'extension' => 'png', + 'hash' => '', + 'path' => '', + 'adapter' => 'Local', + 'variants' => '{}', + 'metadata' => '{}', + 'created' => '2012-01-01 12:00:00', + 'modified' => '2012-01-01 12:00:00', + ], + [ + 'id' => 'file-storage-2', + 'user_id' => 'user-1', + 'foreign_key' => 'item-1', + 'model' => 'Item', + 'filename' => 'titus-bienebek-bridle.jpg', + 'filesize' => '', + 'mime_type' => 'image/jpg', + 'extension' => 'jpg', + 'hash' => '', + 'path' => '', + 'adapter' => 'Local', + 'variants' => '{}', + 'metadata' => '{}', + 'created' => '2012-01-01 12:00:00', + 'modified' => '2012-01-01 12:00:00', + ], + [ + 'id' => 'file-storage-3', + 'user_id' => 'user-1', + 'foreign_key' => 'item-2', + 'model' => 'Item', + 'filename' => 'titus.jpg', + 'filesize' => '335872', + 'mime_type' => 'image/jpg', + 'extension' => 'jpg', + 'hash' => '', + 'path' => '', + 'adapter' => 'Local', + 'variants' => '{}', + 'metadata' => '{}', + 'created' => '2012-01-01 12:00:00', + 'modified' => '2012-01-01 12:00:00', + ], + [ + 'id' => 'file-storage-4', + 'user_id' => 'user-1', + 'foreign_key' => 'item-4', + 'model' => 'Item', + 'filename' => 'titus.jpg', + 'filesize' => '335872', + 'mime_type' => 'image/jpg', + 'extension' => 'jpg', + 'hash' => '09d82a31', + 'path' => null, + 'adapter' => 'S3', + 'variants' => '{}', + 'metadata' => '{}', + 'created' => '2012-01-01 12:00:00', + 'modified' => '2012-01-01 12:00:00', + ], + ]; +} diff --git a/tests/Fixture/UuidItemFixture.php b/tests/Fixture/UuidItemFixture.php new file mode 100644 index 0000000..af3e09c --- /dev/null +++ b/tests/Fixture/UuidItemFixture.php @@ -0,0 +1,59 @@ + ['type' => 'uuid', 'null' => true, 'default' => null, 'length' => 36], + 'name' => ['type' => 'string', 'null' => true, 'default' => null], + 'path' => ['type' => 'string', 'null' => true, 'default' => null], + 'filename' => ['type' => 'string', 'null' => true, 'default' => null], + '_constraints' => [ + 'primary' => ['type' => 'primary', 'columns' => ['id']], + ], + ]; + + /** + * Records + * + * @var array + */ + public $records = [ + [ + 'id' => 'item-1', + 'name' => 'Cake', + ], + [ + 'id' => 'item-2', + 'name' => 'More Cake', + ], + [ + 'id' => 'item-3', + 'name' => 'A lot Cake', + ], + ]; +} From 0387186b89bb53c4a0e13f061b947b561ed137d5 Mon Sep 17 00:00:00 2001 From: mscherer Date: Tue, 24 Nov 2020 01:45:46 +0100 Subject: [PATCH 09/17] fixtures --- tests/Fixture/FileStorageFixture.php | 2 +- tests/Fixture/ItemFixture.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Fixture/FileStorageFixture.php b/tests/Fixture/FileStorageFixture.php index 58cffdd..a04c68c 100644 --- a/tests/Fixture/FileStorageFixture.php +++ b/tests/Fixture/FileStorageFixture.php @@ -28,7 +28,7 @@ class FileStorageFixture extends TestFixture * @var array */ public $fields = [ - 'id' => ['type' => 'integer', 'null' => true, 'default' => null], + 'id' => ['type' => 'integer', 'null' => true, 'default' => null, 'autoIncrement' => true], 'user_id' => ['type' => 'integer', 'null' => true, 'default' => null], 'foreign_key' => ['type' => 'integer', 'null' => true, 'default' => null], 'model' => ['type' => 'string', 'null' => true, 'default' => null, 'length' => 64], diff --git a/tests/Fixture/ItemFixture.php b/tests/Fixture/ItemFixture.php index 7fe45e9..e40f228 100644 --- a/tests/Fixture/ItemFixture.php +++ b/tests/Fixture/ItemFixture.php @@ -28,7 +28,7 @@ class ItemFixture extends TestFixture * @var array */ public $fields = [ - 'id' => ['type' => 'integer', 'null' => true, 'default' => null], + 'id' => ['type' => 'integer', 'null' => true, 'default' => null, 'autoIncrement' => true], 'name' => ['type' => 'string', 'null' => true, 'default' => null], 'path' => ['type' => 'string', 'null' => true, 'default' => null], 'filename' => ['type' => 'string', 'null' => true, 'default' => null], From 3ce78b159dac1e63767596965d17e34437711b4d Mon Sep 17 00:00:00 2001 From: mscherer Date: Tue, 24 Nov 2020 02:10:37 +0100 Subject: [PATCH 10/17] Fix tests. --- tests/TestCase/FileStorageTestCase.php | 2 +- .../Model/Behavior/FileStorageBehaviorTest.php | 15 +++------------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/tests/TestCase/FileStorageTestCase.php b/tests/TestCase/FileStorageTestCase.php index bfad795..bfe2646 100644 --- a/tests/TestCase/FileStorageTestCase.php +++ b/tests/TestCase/FileStorageTestCase.php @@ -156,7 +156,7 @@ private function prepareDependencies(): void Configure::write('FileStorage.behaviorConfig', [ 'fileStorage' => $fileStorage, - 'imageProcessor' => $imageProcessor, + 'fileProcessor' => $imageProcessor, ]); } diff --git a/tests/TestCase/Model/Behavior/FileStorageBehaviorTest.php b/tests/TestCase/Model/Behavior/FileStorageBehaviorTest.php index 5069823..71e0b4f 100644 --- a/tests/TestCase/Model/Behavior/FileStorageBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/FileStorageBehaviorTest.php @@ -24,15 +24,6 @@ class FileStorageBehaviorTest extends FileStorageTestCase */ protected $FileStorage; - /** - * Fixtures - * - * @var array - */ - protected $fixtures = [ - 'plugin.Burzum/FileStorage.FileStorage', - ]; - /** * startTest * @@ -75,7 +66,7 @@ public function testAfterDelete() $file = $this->_createMockFile('/Item/00/14/90/filestorage1/filestorage1.png'); $this->assertFileExists($file); - $entity = $this->FileStorage->get('file-storage-1'); + $entity = $this->FileStorage->get(1); $entity->adapter = 'Local'; $entity->path = '/Item/00/14/90/filestorage1/filestorage1.png'; @@ -123,7 +114,7 @@ public function testBeforeSave() $this->assertSame($entity->adapter, 'Local'); $this->assertSame($entity->filesize, 332643); $this->assertSame($entity->mime_type, 'image/jpeg'); - $this->assertSame($entity->model, 'file_storage'); + $this->assertSame($entity->model, 'FileStorage'); } /** @@ -152,6 +143,6 @@ public function testBeforeSaveArray() $this->assertSame($entity->adapter, 'Local'); $this->assertSame($entity->filesize, 332643); $this->assertSame($entity->mime_type, 'image/jpeg'); - $this->assertSame($entity->model, 'file_storage'); + $this->assertSame($entity->model, 'FileStorage'); } } From 23e9aeaf938bb8bbb97725951d1212d3e70ba7d0 Mon Sep 17 00:00:00 2001 From: mscherer Date: Tue, 24 Nov 2020 03:19:01 +0100 Subject: [PATCH 11/17] Add more tests for actual upload. --- composer.json | 8 +- .../Behavior/FileAssociationBehavior.php | 4 +- .../{ItemFixture.php => ItemsFixture.php} | 4 +- tests/Fixture/UuidFileStorageFixture.php | 2 +- ...idItemFixture.php => UuidItemsFixture.php} | 4 +- tests/TestCase/FileStorageTestCase.php | 24 ++-- .../TestCase/Model/Entity/FileStorageTest.php | 7 - .../Model/Table/FileStorageTableTest.php | 8 +- tests/TestCase/Model/Table/ItemsTableTest.php | 122 ++++++++++++++++++ .../Processor/ImageDimensionsProcessor.php | 43 ++++++ 10 files changed, 194 insertions(+), 32 deletions(-) rename tests/Fixture/{ItemFixture.php => ItemsFixture.php} (93%) rename tests/Fixture/{UuidItemFixture.php => UuidItemsFixture.php} (94%) create mode 100644 tests/TestCase/Model/Table/ItemsTableTest.php create mode 100644 tests/test_app/src/Storage/Processor/ImageDimensionsProcessor.php diff --git a/composer.json b/composer.json index 1ba4f83..7072622 100644 --- a/composer.json +++ b/composer.json @@ -38,14 +38,14 @@ ], "autoload": { "psr-4": { - "Burzum\\FileStorage\\": "src", - "Burzum\\FileStorage\\Test\\Fixture\\": "tests\\Fixture" + "Burzum\\FileStorage\\": "src/", + "Burzum\\FileStorage\\Test\\Fixture\\": "tests/Fixture/" } }, "autoload-dev": { "psr-4": { - "Cake\\Test\\": "/vendor/cakephp/cakephp/tests", - "Burzum\\FileStorage\\Test\\": "tests" + "TestApp\\": "tests/test_app/src/", + "Burzum\\FileStorage\\Test\\TestCase\\": "tests/TestCase/" } }, "suggest": { diff --git a/src/Model/Behavior/FileAssociationBehavior.php b/src/Model/Behavior/FileAssociationBehavior.php index e00f66b..fb2b6fd 100644 --- a/src/Model/Behavior/FileAssociationBehavior.php +++ b/src/Model/Behavior/FileAssociationBehavior.php @@ -35,13 +35,13 @@ public function initialize(array $config): void { parent::initialize($config); - $class = get_class($this->getTable()); + $model = $this->getTable()->getAlias(); foreach ($config['associations'] as $association => $assocConfig) { $associationObject = $this->getTable()->getAssociation($association); $defaults = [ 'replace' => $associationObject instanceof HasOne, - 'model' => substr($class, strrpos($class, '\\') + 1, -5), + 'model' => $model, 'property' => $this->getTable()->getAssociation($association)->getProperty(), ]; diff --git a/tests/Fixture/ItemFixture.php b/tests/Fixture/ItemsFixture.php similarity index 93% rename from tests/Fixture/ItemFixture.php rename to tests/Fixture/ItemsFixture.php index e40f228..51a73e2 100644 --- a/tests/Fixture/ItemFixture.php +++ b/tests/Fixture/ItemsFixture.php @@ -6,14 +6,14 @@ use Cake\TestSuite\Fixture\TestFixture; -class ItemFixture extends TestFixture +class ItemsFixture extends TestFixture { /** * Name * * @var string */ - public $name = 'Item'; + public $name = 'Items'; /** * Table diff --git a/tests/Fixture/UuidFileStorageFixture.php b/tests/Fixture/UuidFileStorageFixture.php index 60b5d6e..1e2de0a 100644 --- a/tests/Fixture/UuidFileStorageFixture.php +++ b/tests/Fixture/UuidFileStorageFixture.php @@ -6,7 +6,7 @@ use Cake\TestSuite\Fixture\TestFixture; -class FileStorageFixture extends TestFixture +class UuidFileStorageFixture extends TestFixture { /** * Model name diff --git a/tests/Fixture/UuidItemFixture.php b/tests/Fixture/UuidItemsFixture.php similarity index 94% rename from tests/Fixture/UuidItemFixture.php rename to tests/Fixture/UuidItemsFixture.php index af3e09c..d6265f0 100644 --- a/tests/Fixture/UuidItemFixture.php +++ b/tests/Fixture/UuidItemsFixture.php @@ -6,14 +6,14 @@ use Cake\TestSuite\Fixture\TestFixture; -class ItemFixture extends TestFixture +class UuidItemsFixture extends TestFixture { /** * Name * * @var string */ - public $name = 'Item'; + public $name = 'Items'; /** * Table diff --git a/tests/TestCase/FileStorageTestCase.php b/tests/TestCase/FileStorageTestCase.php index bfe2646..77d0b39 100644 --- a/tests/TestCase/FileStorageTestCase.php +++ b/tests/TestCase/FileStorageTestCase.php @@ -85,29 +85,26 @@ public function setUp(): void /** * @return void */ - private function configureImageVariants(): void + protected function configureImageVariants(): void { Configure::write('FileStorage.imageVariants', [ - 'Test' => [ - 't50' => [ + 'Photos' => [ + 'Photos' => [ 'thumbnail' => [ - 'mode' => 'outbound', 'width' => 50, 'height' => 50, ], ], 't150' => [ 'thumbnail' => [ - 'mode' => 'outbound', 'width' => 150, 'height' => 150, ], ], ], - 'UserAvatar' => [ - 'small' => [ + 'Avatars' => [ + 'Avatars' => [ 'thumbnail' => [ - 'mode' => 'inbound', 'width' => 80, 'height' => 80, ], @@ -119,7 +116,7 @@ private function configureImageVariants(): void /** * @return void */ - private function prepareDependencies(): void + protected function prepareDependencies(): void { $pathBuilder = new PathBuilder([ 'pathTemplate' => '{model}{ds}{collection}{ds}{randomPath}{ds}{strippedId}{ds}{strippedId}.{extension}', @@ -153,10 +150,17 @@ private function prepareDependencies(): void $pathBuilder, $imageManager ); + $imageDimensionsProcessor = new \TestApp\Storage\Processor\ImageDimensionsProcessor( + $this->testPath + ); + $stackProcessor = new \Phauthentic\Infrastructure\Storage\Processor\StackProcessor([ + $imageProcessor, + $imageDimensionsProcessor, + ]); Configure::write('FileStorage.behaviorConfig', [ 'fileStorage' => $fileStorage, - 'fileProcessor' => $imageProcessor, + 'fileProcessor' => $stackProcessor, ]); } diff --git a/tests/TestCase/Model/Entity/FileStorageTest.php b/tests/TestCase/Model/Entity/FileStorageTest.php index 52c81ce..99a6631 100644 --- a/tests/TestCase/Model/Entity/FileStorageTest.php +++ b/tests/TestCase/Model/Entity/FileStorageTest.php @@ -7,13 +7,6 @@ use Burzum\FileStorage\Model\Entity\FileStorage; use Burzum\FileStorage\Test\TestCase\FileStorageTestCase; -/** - * File Storage Entity Test - * - * @author Florian Kr�mer - * @copyright 2012 - 2017 Florian Kr�mer - * @license MIT - */ class FileStorageTest extends FileStorageTestCase { /** diff --git a/tests/TestCase/Model/Table/FileStorageTableTest.php b/tests/TestCase/Model/Table/FileStorageTableTest.php index 0e55be7..98aee08 100644 --- a/tests/TestCase/Model/Table/FileStorageTableTest.php +++ b/tests/TestCase/Model/Table/FileStorageTableTest.php @@ -35,8 +35,8 @@ public function tearDown(): void */ public function testInitialize() { - $this->assertEquals($this->FileStorage->getTable(), 'file_storage'); - $this->assertEquals($this->FileStorage->getDisplayField(), 'filename'); + $this->assertSame('file_storage', $this->FileStorage->getTable()); + $this->assertSame('filename', $this->FileStorage->getDisplayField()); } /** @@ -58,7 +58,7 @@ public function testFileSaving() 'tituts.jpg', 'image/jpeg' ), - ], ['accessibleFields' => ['*' => true]]); + ]); $this->assertSame([], $entity->getErrors()); $this->FileStorage->saveOrFail($entity); @@ -83,7 +83,7 @@ public function testFileSavingArray() 'name' => 'tituts.jpg', 'tmp_name' => $this->fileFixtures . 'titus.jpg', ], - ], ['accessibleFields' => ['*' => true]]); + ]); $this->assertSame([], $entity->getErrors()); $this->FileStorage->saveOrFail($entity); diff --git a/tests/TestCase/Model/Table/ItemsTableTest.php b/tests/TestCase/Model/Table/ItemsTableTest.php new file mode 100644 index 0000000..2ec9ec6 --- /dev/null +++ b/tests/TestCase/Model/Table/ItemsTableTest.php @@ -0,0 +1,122 @@ +table = $this->getTableLocator()->get('Items'); + + $this->table->hasOne('Avatars', [ + 'className' => 'Burzum/FileStorage.FileStorage', + 'foreignKey' => 'foreign_key', + 'conditions' => [ + 'Avatars.model' => 'Items', + ], + 'joinType' => 'LEFT', + ]); + $this->table->hasMany('Photos', [ + 'className' => 'Burzum/FileStorage.FileStorage', + 'foreignKey' => 'foreign_key', + 'conditions' => [ + 'Photos.model' => 'Items', + ], + 'joinType' => 'LEFT', + ]); + + $this->table->addBehavior( + 'Burzum/FileStorage.FileAssociation', [ + 'associations' => [ + 'Avatars' => [ + 'collection' => 'Avatars', + 'replace' => true + ], + 'Photos' => [ + 'collection' => 'Photos', + ] + ] + ] + ); + } + + /** + * endTest + * + * @return void + */ + public function tearDown(): void + { + parent::tearDown(); + + unset($this->FileStorage); + unset($this->table); + $this->getTableLocator()->clear(); + } + + /** + * @return void + */ + public function testUploadNew() + { + $entity = $this->table->newEntity([ + 'name' => 'Test', + 'avatar' => [ + 'file' => new UploadedFile( + $this->fileFixtures . 'titus.jpg', + filesize($this->fileFixtures . 'titus.jpg'), + UPLOAD_ERR_OK, + 'tituts.jpg', + 'image/jpeg', + ) + ], + ]); + $this->assertSame([], $entity->getErrors()); + + $this->table->saveOrFail($entity); + + $entity = $this->table->get($entity->id, ['contain' => 'Avatars']); + + $this->assertNotEmpty($entity->avatar); + + $this->assertSame('Items', $entity->avatar->model); + $this->assertNotEmpty($entity->avatar->foreign_key); + $this->assertSame('Avatars', $entity->avatar->collection); + $this->assertStringStartsWith('Avatars/', $entity->avatar->path); + $this->assertNotEmpty($entity->avatar->metadata); + $this->assertNotEmpty($entity->avatar->variants); + } + + /** + * @return void + */ + public function testUploadOverwriteExisting() + { + } +} diff --git a/tests/test_app/src/Storage/Processor/ImageDimensionsProcessor.php b/tests/test_app/src/Storage/Processor/ImageDimensionsProcessor.php new file mode 100644 index 0000000..4c5c972 --- /dev/null +++ b/tests/test_app/src/Storage/Processor/ImageDimensionsProcessor.php @@ -0,0 +1,43 @@ +root = $root ?: TMP; + } + + /** + * @inheritDoc + */ + public function process(FileInterface $file): FileInterface + { + $path = $this->root . $file->path(); + if (!file_exists($path)) { + throw new \RuntimeException('Cannot find file: ' . $path); + } + + $dimensions = @getimagesize($path); + if (!$dimensions) { + return $file; + } + + $file = $file->withMetadataKey('width', $dimensions[0]) + ->withMetadataKey('height', $dimensions[1]); + + return $file; + } +} From be0c394eaeff32087636f2975bc0bb53e0074024 Mon Sep 17 00:00:00 2001 From: mscherer Date: Tue, 24 Nov 2020 03:30:13 +0100 Subject: [PATCH 12/17] Add more tests for actual upload. --- tests/TestCase/Model/Table/ItemsTableTest.php | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/TestCase/Model/Table/ItemsTableTest.php b/tests/TestCase/Model/Table/ItemsTableTest.php index 2ec9ec6..088df97 100644 --- a/tests/TestCase/Model/Table/ItemsTableTest.php +++ b/tests/TestCase/Model/Table/ItemsTableTest.php @@ -118,5 +118,64 @@ public function testUploadNew() */ public function testUploadOverwriteExisting() { + // Upload first pic + $entity = $this->table->newEntity([ + 'name' => 'Test', + 'avatar' => [ + 'file' => new UploadedFile( + $this->fileFixtures . 'titus.jpg', + filesize($this->fileFixtures . 'titus.jpg'), + UPLOAD_ERR_OK, + 'tituts.jpg', + 'image/jpeg', + ) + ], + ]); + $this->assertSame([], $entity->getErrors()); + + $this->table->saveOrFail($entity); + + $entity = $this->table->get($entity->id, ['contain' => 'Avatars']); +debug(json_encode($entity->avatar)); + $this->assertNotEmpty($entity->avatar); + + $expected = [ + 'width' => 512, + 'height' => 768, + ]; + $this->assertSame($expected, $entity->avatar->metadata); + + // Upload second pic + $entity = $this->table->patchEntity($entity, [ + 'avatar' => [ + 'file' => new UploadedFile( + $this->fileFixtures . 'demo.png', + filesize($this->fileFixtures . 'demo.png'), + UPLOAD_ERR_OK, + 'demo.png', + 'image/png', + ) + ], + ]); + $this->assertSame([], $entity->getErrors()); + + $this->table->saveOrFail($entity); + + $entity = $this->table->get($entity->id, ['contain' => 'Avatars']); + + $this->assertNotEmpty($entity->avatar); + + $this->assertSame('Items', $entity->avatar->model); + $this->assertNotEmpty($entity->avatar->foreign_key); + $this->assertSame('Avatars', $entity->avatar->collection); + $this->assertStringStartsWith('Avatars/', $entity->avatar->path); + $this->assertNotEmpty($entity->avatar->metadata); + $this->assertNotEmpty($entity->avatar->variants); +debug(json_encode($entity->avatar)); + $expected = [ + 'width' => 512, + 'height' => 512, + ]; + $this->assertSame($expected, $entity->avatar->metadata); } } From a2aae98f465b57ea24b4ea95424a2a4e5a641450 Mon Sep 17 00:00:00 2001 From: mscherer Date: Tue, 24 Nov 2020 03:31:27 +0100 Subject: [PATCH 13/17] Add more tests for actual upload. --- tests/Fixture/File/demo.png | Bin 0 -> 45172 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/Fixture/File/demo.png diff --git a/tests/Fixture/File/demo.png b/tests/Fixture/File/demo.png new file mode 100644 index 0000000000000000000000000000000000000000..ded48c3ac63747b1459af38781c6656007a23d23 GIT binary patch literal 45172 zcmce7Wm8;T)AcY6F!NN^7lAVAQd&&mD% zhqvlf!H21{d-v(xy;iT@@fvFKm}sPEAP@*sQ9(uv1Ofy9fG-dl6#C?l|H!=AG@cX_`U}slN(C@Q=Kf#-f2foF|zdi;YuOWhB zhqm2?$g%!E|ADnqY`NnW-rooIs1X%#C=8Or@(@^UG$OpgtauRu8ZCF5_K%# zx*n)<`s688FbqcrVcFx?IaKGPZgHUP$v$*3Z-Ecj$=Xlu@>d@A-Gwfa)m%`73jA%4 zzDK7+8S)YKhIbyI7PQoYx;hyxg^sSP^*7JJV!=anAaXTg6HWbW%2yT=a9o03aY)Jn zS)21}`HjyO@S=AmZHVM*qPVZN!b+xws6ZH?r{nJx=u96&D1^w>9tQzq%Tj(S@7IIe z)5l%QQ8Rjd7j*ePIhkW)#o`tJ+cR?{9Bos76%_;;5h`ANq-b6L_g>U#ge`ac2CK1B z*^Kcs7Srtf+=!R}JWXTM#KE;!^oAy-a9k#SSKrfq)hoigC2@$NCDfGs;~V% zhPVKW2M_#P9R3p%Jq%^0%iwe_?Ni57CsK7jKbFY$cZBw*uPf&d`fVS|>J`O+dXGn7(|Xmr<~1M0Hw|z(W}Qh{}xi4np#=M*0Ah6Ja_wf zuq9pz9pf-t!c}P)2zZr$Uf`Ct2>{ogMi?$Ib3)w+Q`BJRS3R{Ug=4&u=Xh73(AQr3 zDv_P~@k86g=>MWG5thQMxB8#Bl})z}{pY8(e|{RmuuVE*Vlt-v^WM)?KlYoq zYyd>AG&^)U87xFTrSQ+h23kBjgcKVd**!K6J37ja3bSQerAm8x6ty zvqf=$EqKU2{E+>v{#H(vgmChs-XUA>h&__KP=WIQTgn(hy=<3sC%EagD_vg2?DXJ< zYjdB^|90`*jEWrVAmV?kr4LwTIKS@(H5g9%Z!IEMOmLD>o&<^_|A+>5M)PlHCk?3Z z@=-k{4)pf`ubV`q>>xb#w-Jx^4OfbR%m6Z z;cgh0xn(>5@84>C|8qYzRO$w|pIH_Jq8_pPVk$V#Vyf2{JQjgcwvQh>6tDd66cckx ze&qy}ra=6~(e3rEZ|2c#-!cE)PLO>k_oFCfXqz5C@LwvKw>kR$-+%DW(oCsnXlb_| z?S0M~HsREGl7w)KcrCoQ7-B35@}Pr`m&y^Lz$QFObGVNp?!s&_LI>@yDZua!;P|Qy zUY<0=+FWXDzbkr+5R7`G>7e5i;8#Z_(t}EhLpsxypNq5Ug;{u2;heI8PPo52LlHuO zbb_WgJCV)L-fg*?IJANVlE9BZl^_j)Y(MyLV?bR2gfaq%`u+dmD zKJA6+N>tl6hZ=zttUw>&D8QpNimha;VF;#yL`*@Ys$w@XAZ5ZNB0anpDTS$ttMx^( z<}W|R7$%AgHZ=5Q!hiUb1TDHOd*Fj~{~(ZKF+FR(xQ=aea|7q&ja`#119VYi%dgV0y}9I{cg<$XEsqS~ow7ms_ageGY!ZPLOvur$|` zzI>76`C~q~?QVq$g!W`ZUGL{4s6Rgb4@B;oQ+1&|TgGi;?5X#M_C{%57K)U@cQi`4 zy^n5OrDgU)dB)_G)L~31FQtN2bwLM(emP_~K&^EF4y3&9V%3$~@pZ8ldNzagQI%cQmMpU+md*3cwd*prtRU+7Y8nN-OF6|ZyGYL?X*H#V+WA7~( z+D^oxMA#b6xWB)DRYol?$5IrPpcrBoNh!=?XH$`rTIejN*MzjpVpndYsvvM|$wwwB z{(R|2^2V{uv=i;D(9B#J_QRODX7vktUU8Pi~_V&?9gATo2`W9zov(cnH|dXIM;cFINBM1$&`Zc!kLQp_(8id?Xb4 z!)K=0zwLkx1c#>WWFEiBSIJvb{5wJ^ts4vys4Q zoHsBsZyy(*c`62#;^Dmzm#M|ZM?21-jSQ zwx!hKem9j=6lp5R-i&CiVlJOODQ_A_vVzPb)`!&M-YOy} zBVmQLnJ#B#rkymC%-M%9aY`gG^)DQF@iOEV+mZ)8xq)(1 z?@bD+E%amw7pJIVkL|L{Ua^rNn>ugYNlX-Fg|=XINydR9X9G3K z6w@xAdl@UI(^*EkJ^te+^JboGvhCJc4&QtDzmGqwG9*M7OQ0fn*Wi zI321lkyvSSh(V+lTp-d25s`w^CqHh9uOCM-YaaT%#4!_lgPq=e*EG&ZTHV*(Hpr%+ zDm)r`Zjej^#Y3Gw|x`5A-FhB zjG0zER*o(-znJ!ioM#rNzfUfq^p_A>@D2+oNwGw)Zp3Gt{6VKg0$?Dqe%8BP`%d{s zcEzk&+uIWQ%fTbE2uTg?jHJ9$@48H8tQJg*gE30-qlQR1-#~X7hO21cd@PQqP_A?b z=-aDsvBvVr0q!T4t#dNa;7|(sgrR6-x+v0#{rNUCn;Fhr{vFc4?Z`eGwGzx@0pDU?h_~lHNuDYCriPMn_ zb@Ig%*kRnXyKx902r-xBowRO%Vwkq_YVVSXDwh4pKTw}*i|OO1mGT2YOJrr5_Xvr- z*dz)iOJ%rY?bRXV)VIPctU8iqtuWJWV232S=8hRTgxe^M23nk46eSZcjKe)=C+Cvd zB=?@MjB@w#dqYbbtio{29YqGC<1uw5kEEZUfPi66Nr`Q-wWUSoBvQDbF{#b!6S=DA zKR_=TaPrF_>R%Ww?)C>=8KyHMGxJ*;`g*chK0N@fOvxGo znJz)$j*q)@TkJ>=&oxIod^z*Pt<;DHmPS3{?AAb-e$V7&W>1A-N9NtBj?KWz_HD3X zh;Jt_SpuS%MYm|f@_IROl_+s&d`#9qM4e6C={K26It?E5`E9_Pbrae0}recj|>&VN-V8tdVO~5I)Z`is7q<(FO(vPsoMI-bcIhxtvB zAK9J-k1F{lAl>q-^#Ayr8y=5zlzKO693t1`$@ z-}xo(+@@wae<9LWAK1u9YiznaxNChg8~9+kL^0-8ssF^o4-cXpJt~x zrLF=?RT^h;7D{maQu7i*MF#K*s6UP2QaOGPpr^Lmp>j0~aZLIPJCzfxmh&TL#xH-D zLQb%U%86CLHqqm}-ANnj&u$DjAZ+X1vRSJc)D9~nmfp~H6`lRc#Qic}h~ZL4Y47jx z9HFUgdME(?8@!JO6-8K2KpJ?z?NvCZ`uj{?iMm?O`4hP@>1bO&F&%5=a_Q zID1FH^EqpW%O>9L*0dyNS|)=LQk?Zx*Qd7|Z{ZPU-d0=D7VLp>%m+zHy@A)>d*w!h zPsCyfA)$ZM{<2y1Ap+=h3v7YW+-ZnK8>pgq^(B`9TI` zQ=XIKVjWG73_HhoqoJ`_HF)`gm%y8Le*M-ivC?T6%G%utU_G@F7k42KfYn%M*kYpB zgAThugCfD7zNrrU3!(7>&BApjN<9P%fnSyaPE4rh!H=I2U=7I^6=m%6R2+BV>PZ(L$gA_#-*|V+7JQnjc5*8Bxv@RM(Fkai(hWk>a$OQ zc|zGnFUisSRQx7Owxa;cE~rp$Q206>?fhqLCCv!#;u(A4;hrDhx%}E2aUt?ggGl|v z){Y_Ttl)cnP(rZ*7~yliZqh2Dj%=NFK^6`JT5K{0xMbw=<&uPy+@@_oi~o(G9&fMb zLW}z)7!7Ipyq*886R!D3Pa(^cJ3o&pCib@m{?U@>q=WvKf=5WAEp^DXm4vlD?{j94 z&v}`KE2^9Xfwq-eee&{R<%dw@{^d$7^Ij`fr6xH_?O=rcRg}4~k{e1Pa*ltNSq2OZ zR#3%05lDR0`XndnHS$V@1C>pm+x4g|dF}NJx$u#t{pYWc_K1D)F;dYL#YW>JUx&;}(8mcCG#W%NQ^(@v_%1ADi$BKIqHh)t*G>dBj zqUWytbv$8{a=ktOH`|_RcS2uHwo`Z2es#8dVu}%j5r~jmM&*phFB4yCALqgq;HW37 z8adzZJhL00**dDK8$4NW&|Cf+tM%!?y(Pl8Lfl^Ge@!3!;l9XH$)lB;ZXMCaQi0D^ zchQKMwhC!LmZVO=y{AaTp8Q51rT>|dED%{Ng`}ATeXcbm!z0P287H~Edf;|rsmj3u zkGZ!m_-}-&9MuKVH-&6l77Ns0-Jp^(onY&)SlKH7RnAJ<`e0cmW^iDYq+BY2w-^A|QjMd4Hn?2Z1g9cB4@6WfdqQR3g|6yW4EKn)0r= zVs3|YQ3r)YJ{t`>t0HSFktGldvU6NOUvwdV_3r^$q#peN8kdL zq5<1GNN=7?Q~`Be8Qud~N>9rS%XtS@V+YohGSHOh%hGy8(8wArEVLTHV`F;Td37Aj zTyg%l`~1K4^ZV|UGWQ{DMSMicwpcs8JQ5bgmA3_6Ng+)4xh_luQL{GkbU-F{A4$aR zzPD^%`OM4^k1K-RFPhL?>g!*!+6rW2PS9)62~=|(<^B8juB{Y5=_xNfejJ#jWwIUE z&Xk;!PQ_5?DmIh;E1Y>$zuq7~9>x9FIQ`dMM|mG?y4(+aqjQWw*+fpar^Q+px2cqN z56&SV#HXHyoKM`j95ZA)$2Scvm9Ts)zrmw-XJt3dVy_R;Ky09XNg0oTr@;`|p`At( zjf~H0QZlmT*RMTutu1vYk@(rWamPEB0@1haAWDszS!H%;*K=T3#2D576{%&Tk!2g>fHQBg zD((z7av{8eKidDgtm7RxF^V#33VFmC9y%f^v-#(~^OV|#E^{>Iy>a_*?NuqY;6WtG zg67?x=xLC5W*UEl{VXgc-I7QC8{;NzSym02h&_cyEqO9v zh(!Eyz6rq27%wI#fJVUSjd`8NSh(|AW(5K#*OX4g=TMR*e80+((jxVfYSi(-X4|_z z{N_NrFa65Z*ZlSe=gpDc|9Y1m3E^>l5RK0^lfx0-YeMpxHInlw^r)P%h@4f3O?A(a zFXSAFZZ9D2k0!>N8D0=+Uea4{-c^13e9QUhTf#8C{=z4}SETP1l+?mGgpndjinza{ zLTW2Bwldf4b52m9`$-ewYSiZ_)6axYG&sE_kT3;lYWe{p+{{+;MPx$ITj*#Wee+Z+ z&;|X$Lh!89?!OviUo7j*=P3LSg?NnqxkthV8%bI ziIlc||Jx#_j9_-WR2}Ei+Q)*+^Ab6M6XipC zMqRETCy`gJr_O~wt}F&7X8&#oUM;$zy6l^>4{F>bU}Zye@VsY?F_BeO~tw z4ixUY-t~B@Jy<~B1BP!MDpM?r+FMgLn4Qh>F$jHMt`)UDfkY@Y% z_%qVIW4Zozu*n9$YDB=xJdNI8%=NCvj5rbb@OoA(>(4vfB){ccXW96jCXfGA+ERyY zW@rQ#BI1!8b0oyE*{bkAR0<*fb<{hAfMdCcCwh^=2#m0Z*K8BYi#j?PcXm@s;(=EV zeT-9_LaEWfx3;+cwZLeI@kk*la7-Gh)}LO>NH;{dN*2rwlQCCk5_R>-yNgnu(*VVG zF4vTQcV;x%)TsFQ#lPOwb8X`VCo0Spk z)ADWWoIS9c*eDVdrb@51@^g{jvB{?h3q6P`<)5h4(#)v|h0x)n4V-#LL<~9z$ajBC znY8KO$U^8K=DlP=s`;apnvR=qjiU#VPO;=G6Oq95SqK0KXjCZ%nRf)JiwO>UyTlr- zCD|Z7pBfAJQ@i#H5rvbJ(`|K#sZd!gI_Q0K(+5{pq)=?67rnWOCimU7gq**`7?7DC zb4c|};#2n$@94E3pP~?Ht#hvL88Sze=DxfcJVM=J;`$xSHCl9+o$cIT^J~7E@r8w9 z>vrhacff*s&?4@n5k5gQJVuFTwIp(MvgwR1A+tKR%z zb@HUdr#Z{7l5TD$z1PKM3zWmpoNW&|lTQ5{Y+v4%KDlUQobM80m)NH~QK~@QLtK!x z_=TPgvuptrqVNHxVy8TuhnjmO;w&+shY3rwWzU1H%k(SI!XoNy_?+JHHq^C2wUv1- zXV8iNKuQfg9Uu*o@_X`t2R$d)E^qk-{r<#Bk`E%mV@UMP9x>p6I*N(_P?UF%Er&l$ zl+)tEdnr2#oZa+{?1GxnLnzw>4-L~jd3y4M3Fw?OG#>BWN84u}TmV89_o_{N}fD`^|hSkcyCgFTHC$JGIvVV)r3AVmr9g&_4j6R9&xK9wKd{=I$Z zIXwQIkdN`AjFVy0?X!lk_^`|Wjx9AGXSGs*Xy+$pRipMmiv*~F#HYi<6B&c_#QfY4 z0FND})F2hYiE1o1C_>)7$)QhT!JZ<53cJFawl9A`gUeoef0jLm82DvJ6&w5UU}Xzl z{g9NA(xl)IrKR)QT1NjqYIHkk65>D0bO}bnkhxPA#5u;|H!6-HgQ5 zT2I2z5+r)Pz^gaW5(nbUn&iK)>F6hzRRtYsQ&R(8G6s)*!w(@jDt05s=_<%|ua~BD zB)GH_Vu;BrKE_Kz(fFLJYo~_h(^M36gd;GKp;9dH2SCSH8fUwd*ou!>0mP8^ zz#PF}A*ic;YkMxHzjFXbV$9*AHZnN$%GXZ{w^g{`{#EeIZ>g?Gj$|6P$wdB%FSU!0 z2S99HY?$5P^ktV0;S3%PfRyG`8;Eza-s?iw6IBnjvTUgdz0@ z`*nG3`)3@;9CUv__9*MCdTlxNFfCt+Ur#{-emil^(ELu*v_>t&Kd2r(%>ThaMq-X4 z!=q#sy^PwSlqLli94aBh19_XKI@`G~P9zc|7m8W|+SmI@y;+>f!P5y7LG2cAc#3s{%*Y*EKhx!ji#bFA#aG!#*U9eL}BRS&|%6oI*aw z*bw5U9C11%^T)$N)U2RG_ojY}Na0lIdkZR>p)LEY0v1+3*&x`ps58Z$ed>6!D zw|g5K*nKy&fgnVZPSYAOeJmU1N7Sq^+0^(#c9U$0;En|dEYBS#!N*^gJIY)|dDH2H z^rI|VRlHHMISkLgO;^*h z8cF=8>EPA3;u=@`4u_w91?tIcQJbOj$&1@}zfQMqA7(bcW~~=z4swE!P2w}pMM%BD zQl>tXy&=2Kb*9`{N2z4ob~G7}u^}Z7PZi2UYMMdC);OQLU-dWLcLqwIyXXbwFT-QA zR1zH8d6ig@?*q4dzS>KKF?5GI6RSzAu6~}h>Ziz7k5U`i7v?mF#;P^T+IUC6ZW>h; zN`<~3M@vyNCzN%Ac<>{bV=NS#Oq(oIH0y`DP?qnR%9(PxYfYY#l8vOPA%I3;Ale85 zM1xOHpA{I;31JEUP7CG&-do=y@eq+-X-yA|WRL?$`n(SJq1HZQfONp@H*We1N19q`x~a%(t^%n71OPoeTa3nsJK@z1;>z zc!+9yzBYoBLDk>JE6_=qZQbs*Q;MQq|N0_Z7h_zB|Fj;jzZ=H0A>{NmCHZv!^Bm4v zkSZoSEB4gvF_bkytMS%<6X{_Y54P6iDVjCO5;N&E{K$d?MP_*A*L*8<$BsyeQ-uO% zo{lTl8j$SAj@$s19qm{-uEulT5%aEoVhl0CMAAYmGm8aU^*N@gm{nX+uJSUTy1bw@ zO-(&3W|rEeFUDx{M4TX<%F?>MTM&zFYhSyi^zeS>gpOR8`fX0Y-D^bL#bR&xb&s!A z0P_C)zM~blN?-y>Gvm*JQ*=vW7@obS;gf@7| zZ*+vz6~3gJQ6*?4D)4{(`f2dsa?naOLM_a4_U8GcxKJ>iJp2kg(6mC^+Kxv=pNo-W zSo9wCuj3){A8yl492j(fmLfGfQy1UG$<!5HD~|4VR*syyo`r4D?ht7cktwd|_85i4q#mC?SqrV< zhw8?fQFl05kuvTNe}$9xm?d|YS{+7Bzsq_FyoDw4e1iZ<=WeVJeSSk}O-icqNrubp zr5cGdOa@o!l-l27X#bR}wx?Z&Ufd=mLd(qP@gaHDHVKL_@J5-5b{RK!ukLxn^)AGE zcN-h@&?$OX9VzxT9JUp2TsdGjYsN<`4T>~__s@KsvYEG3Q>A2Q6iaHyid|5btn80R zkidw55sig6vA@BsKo>w;*%XPRd>H-Uq{Q;MV2V6vHy8s!$}tNQooVk)1`8l#m7Q8z zA~#evm3CBD3fRw$rLWVqd+u?Ug*tmB{+kG4h_axmeF6$1etB}}5;oGz<`2YT!Oqs7 zMpjEqR_PH%FCY!mywCBCeW`Gm#qyd}@I~8^LR-u&s0x2+ZLhzJi9*v+3ExrFm8iRD zTEFFx*vo#Wjw@}yf!N1@uN}PE9m?qRj&?>WJ?-ID5+! zY|Kunw1tBOsOjsG(B187x}t`>tB#NXA_!i&d`%yb>HzhrL- zCz*${f9E8%a2t26gy24Vy2A!W-zU8n%Fq&`c!BjgCJMt88HeHs2n)`0jMCx(#SI<3 z_EPS)UCx6JGr+H|rk+bXNc{p$0vyMqYg=KCq#*VV`09ZAnaFx6K@2Ua?+Q}snTq2b zL4Szd(hyqPU(8&{(WsmnHE$hN;IRhIfLhz~wtI87LeUUe+Ux-i56cgX(%G6F4Q^=P zCU*NAmLZ0v`PWf-s`Nk3)d~#f55ds8Cny9Ajh0T4rWtL*V4a|jlBuKlVK7vJ$6Y~> zH2XS5}-R zt^_48-PQBOsMN9ckwMxURyNH9v2xMma*D7k@I%Dbp-8)LJ69c+*SEhsxQyG88cYU? zdK~NIy#LkqsEgN=WU*Lo($OQ;;>)Ivb>fbk>NrccTG`W!T3o?96on;!7_poDUL@l9 z?VM(H$tD6Io0P_BaK{0i`{Eyn$*V}&a|mD0VZoGwkK$C)1JpK(kP1kI+SY7jawG9o zhqDP5AU|sNY-i@;_RBzBKi4q}dsO~+S`AoD1hlm%%A2}@GUI8GsLboq;j0(0WG)~& zgIzCp5&KXu-6&4=9BN|NBSF(Irmrq7D^;`dyB3S4f3Jo;2SbG1jYO;a`N{&MYiPHW zxYGOS&{>IXMpy z1LDM976IxlvYtbL1cB`S(c)?3rufkVNO|DT-XqE*vJbZ()wVg~m(1fPj-vU}Xc;jM^ za->4q>}Hx>O1&C$!Qd$5?kSlHJ%7i$MuWQBykqSOUj%xu18^#{WHPl_3d(l$0SizG zeuNqd&9|mW|H0}XmTI$=r8>VoANxKMo~N} z@?YWQk|x)kvNp?CqiWwn#o>#owj>`cU}nh&Cyn7>F?qb(D0Zi!KI0%U(QrYjs7m@1UZC8;sV?3kb#r-Hi=gC{QTV`^x;*IRXUUQ1tFtRx@Ip`Y5~ z7%A|&>S|^Rk-4=t%qbZ$`Guh?XO8l`2Lx1(j{nN<6TzVi+Ri!7A~$( zuq5I;m&(aRV9r3}>Wg>VMjv`w^`oi~CCN?v-CVEKi0L!P;#Y_Bnis$NJvs_Ff@PN% z0JBu0Xr-JKQ`C$HhWui$m~m1WP!amD0c`-G7&}9Y7~@OF^C{UC*iPTB%z7qLylSUR zO&UJIHu3JZ*wInq!;M)BmkV6#^CO2i&&#+sp*rY@eZ( z$#pa_6C~H=g2lpM(eKTeN=ms2y0Cos1mC9G^pm7;^Tg^SRYx-?oCox{3!gw^A^?%) zD&|gQW*!x6mt}7~b1ILrtMl~|2J*b_NcirTpSRixY@ZO&`4ZTTTO33n_tWfcQ6OMw zE}0(hfvR$Pci?;mzOtH#+VdM$v?&2mC&qq>sG}G4S$$>T|K+7xQCT1y7J>KagntEq z>_{JxZ4Bmv(C5;ByxJ1aA!TmoHI$c!;5omS>U%*e@;kctstyq-ozS z{(dQw)tlscqwA4Z%73gbf|K?|E=frgh8Ar>MJXfy79HScu=CjUSaSj>!s542GDE`+ zb`|(>GQpR>kkHc>Hey%Kp_2F?>zJQVL6sTLY*m@1^b=nmm)}G_S(om)+1H|<6}ZRf zqB!#(UsSBxQNNCiEv7@I>5ppiJ`r6)0_5)BM7GeV5}hH$$S1CRGM?f-+h*%@*Z0H~+pQ>kq3&yXyV@I%-#J8oH3ys)=@`bM5UZBLV%I;z zD$k=NCutRN0rl5}^I*ALDjgVpyXhkUs!AFLMkjcC50ZErWO~oXacbRWObx_WX;aG; zj&wuRh_98D2!QPPLO!cZ`#z-qY&fugOX;TZMHvOfBapkhp&aOZ61n9keecF^ZBLuQ z|H-)Pf6N_I;QXS-izLm<&B@n|nq9CY$s*f-7P-0&XCLDxexEmmM@^lhl^1Omi)}ij zBnvXsFw<-5`mf@&W#vHw@$|X&i=Yg3N9=;Xks;$H%4%$kxFb?tT+3_l-Dy#%x6mM& zb%7J#PHrpTU49q_HKfDEmfu_C#{FWzl`E#(x=PnalujG1YA$K2jS%DNmKOi}ZwHTX zkhy`$dw>KcWTQdCOiazq%Wo*>;qsu;{{InE26LN62I_yEg>NybFDzgLi5NWZajJ^` z2N&0W-O4cL?nAkn$+#-l{Ge&grow-cks1xg0VAQ&=!b9?zIJf_f^ zw8rvYD`DWdH|pg20B!Vc?is;Zcvt6@_kJ+1_T=54LO#uu5vu3L{qg9ZqY9ST$5cm` zWz&TnnsqYKVD~Q5!xM)gpwl6v8T1kZ%vC3I*2*Txdfbjp@M^JdYOcN*l!=iL9n=}{ zNTBfkWdK!nHUqT%1r_wLFOXztrjtD}!&So7^kJWd5Lz%%W`AQ0;-tWES)3VZIGfg66_|;m zCV&sF$epK>H;w}!b>i&ogmu*rC3EI|eO`7GG#*24ru8-K9`jW9=vsg~v-PKG$5#s#Q zKs7QH!(Z0MuAcaBP4&Pz_QYqSLYwn}knOqgR@QaOKXOV6jamqs``918I5a~MKT_`>u4L`{nD z;9j2vbqY7dOH2r@*=yd!+f^}Wbig8Zf$D0Hk1qdbU;>$PU;*nt5ew7uUWL;j$fRQMiQs7oyUF0JzT!--C@fF+WVpER2GYQbY?}er;e@2b@6VWVB+u0 zo*fKm%bgB@HFieP4W@ zUs=g`YAYfK;{?&l=*n(uL-yqK9**rZlb7y)06Ni;1zY`h+6=J#L&)9gwk7%5tu;Q^ z69^vROjgwCLpqM;A4&BoE!9a*I~EZb(GXZbG{b3r=M0m(Xb=-4?mw1~mCIE26z3R1 z04Su6Z61v~0ZhP(8Pa~~tS^`F;Xor6^JV?T2>t;Qm9bc`mw^LkX7v~6tJkj z$#z03p1lv_OY@6PEsh^YU5tBtSCNYC)$Pr&%h$er8i{9L8{!}lvGaFjn?u0Fu>biz zkR0AWyQJcoI_n{NaY`zb0*F5~pWLiti6G@-@OeO!g2gm0G&zoM2q5e&{nZDw7Ox;Z}HGJs)Yx9T8U*Ms)* zO4vQllvttf>tP{`C%@N`0Jx<#6XK5AfX)XR12oF}4Tr0-E+(D&Oai(SD=)#dwa^US zMmGvtf?zVHyo`leoU1+brM-$u>~mBS_$x#apJ(PrJM;`~_0 zCtiSCm9EGSw z4wL`Hw_9xp2_pj22=F-YK$#V->qn^0qt1V&ZfhQ7Hc)GC`S)S#^}r+WLm>pH0DS~X zav~$&)%_A`Ek-^`QAyl>D1$tLjoS5ap83Bf?sklUI|4Ua7XSRZ9K611g(?5Nzr~o$ zA=mW{moo9;DblO8EB(s;S#hcjA-k`i1q5SgueMO&rQ~FkrhG^~b7-8obOW;j5t3Wz zaY75O_Dz1sHs4rPtj4>EY!isEu?7j^TtZa<#TvJQS(WbsEgVX%hzs~h5@(Knz$3u% z?ofrQTWbG751HW28&wXMvs&NAQV0>qvdR0XKqdUw2FSwN)NB33GjXEJTWFO>G!b6R z5h=0=v^O?5x&GxK#C0?Oh}13wvTG5O<_=sxbAFs7wEJlP~N+WOum& z=293?5kppj9;j~OmlpO1^S_=5ZJfzVCf0?uX6E{z?0?MRRX2M<&;vAy$xw z*2o=VEQyl$-)oO6S+l*{^sLwS=H&)!2!k!xD94CZjqRwPl){# zKD(MZ9!K9Cx?(n-8FF{_Btc|quNy8}@FW23a3 zju45vkH5@GYIYlK1JGiHR1iPewg0chqvCM}ny3<>XgUhuCB4FqS&*<;Vk)&|j=Nai zkMrT(E11NDB0n}5)KP0va_i*T4lVAAp{0EqYe{QLC~j|Qp;y5NZZt**7hJ!Pg7%$D zR!T8UUe}(a3E1>pX0W{99Cm11h2x7t=4@}3r$ zLPg6FbSsi|o9TH2*IMKUa76N_y66S(BSDj@5aQ6xjc3{1op}pTzWVEi-{p9xH(Rny z)$fs|sH^jh;3#f6i2z>>j+-V1ZmY~JpGoOU82@M1q!>1^*pCv}y|gec;CN2X$F#S# zazC!TVpCzrZ7q$J)Q&OqQ`f8*K=?q7Dx;l?qNnRCltVMDv`>aGM~_*(UrCh=P~-J2 z_+?25hvevK>0P{&i3gvX*MS+|vptBx36vI1#A-3Ll{nYw6e@X->qt5J3o}-BLKsJ%vLW}Uy%G*?Gxt+~&-jc>IOQP5jqmItzbUF9Pb0UV@(Ri=}rA3tU5jmZ~4nW@C)X)zD=wIfZ!3=M_>sxJog zVk1$QXYG8bCVp_MKug?2s^Z!SeE+{&uo!|kZE!@aTTWdy*#|Qwi<@#24B@B4Lv2Dw z(3IS4()EL8mYsA{{H7jVJV03k*BT7iXO3uJ{%B<6Mo};Y;=qd7yuUp(BRLcXhuwA-ryn=ev*%i%2j9@AjFW6l%I#)A=+r(B9V91Ly>*O z?ps5?*qx>{QKhgQ399@N_*b{wgjzy9YO&m&0d%Gd0R0v?wew*^$D58)?O(GI`n7a= zRO=gR*V%f#YxI}tX0`co<%&bKe z&EO>jn2-T)F$dvOPo8<5NzASJnw%c<{_3ecg)bV;Y6ueb=Y#OYMix!Cl55deTJhYR8 z$(^*Q75J3NWF`1^$~I>lBZi7gq1{AMUzkCX&C@L{FH)y(d5to55ng! zE7r+LGUlwc`b8-4YKAKHg9Ks`49okWp=Tl5hlxUH@jyPA4L{SSS6MSBJ&Nm7Ap$yf zq~{dfveWQ>@=_i++Q!1Dmw@YpDoSuTRJ`H`j%dI{OPox>IJ%ZbhV{_E9AXN7R^K@O zzZXf>;2fDiWD0;*tR}`l-tgN0vGkQ;Rdj9Jv&ju2jevBgG)Q-Mhje#|(%mK9El9%+ zf|PW3Nq2|BrlsQ>pZ7Zse$O7(th&y+LL4Q*(Zt*&aKjBu(Md^pf+^Zp7CMQ42-zrR z?fF&g<|g)8AF|7+mu|ewaG7rJ82r*IVbB!6PhS zCVXH?V`?Dx_B#JqM%Fa5Krv{?&wrPOD4{z7DwXp;`SRQQ=88B3^E#x@Rxm-u#Z#F)3S8) zg7kGXW!hu{z~fe<+Cs$MOF)JB!2zp%#Bi3%#VG8`=R22=w?4z1T0o+zc7I72l; z{D}`Q%H>VNy=kMqK#6tnO7ZD0&3EVF>z5wJ9Q#Cs`X+cZc@0bZ6JsKRY{$ZpFcaLTBA;(dibz~HeA9!x zop1-#{lbTW9d7wf5~^OvIw z^F<Sa1a7W7X zF(vy+C_t1aQGy#FAQ+lmYEj4~RA=cxEQ%I&`1?fC$)5r(e0q05P?mgX6nm!RD@?flV2imn z^|A7Cjg`;JIijMfMhU@Gv3r43iMdwY??wj)KVg6XbDo|6{G?E)ifb>qTFLF`SFu$d zZk(`kaHYPL!v&$)sMSZxLm77M<`P~KLOEJAFF59X{))kOK%R!r%N9sgPVD?s(e@W? zt;jEYM)#WHKJG49!2pHB0kYAy`Wr6nnPX-8KcH|F1f*8lZ9t&y+*0$?gp-< zHo6DqV3rIkfDp7fon1mMu!empMDUt_EGs3%e7x7CIaP}>M**#BsQGq^O8FKf$Uqv$ zUj5@D9|QpbRA7|)EQKSekv}LZtgz0l`|zIGG9PT6XI#u(Q!HmF(vD>tN7Oub26T_s zbVU97^^xm+zS>E$gXZR#ujHDjv;yannn(>aPn#Y@e4u z8Mwh-d+vUxb6LPY>CI_L|5|%SW^FE9gKzAAFV}2v{N5}Gh+h+YP17=L=bVT$xNE6* z{`~c%wnCqomIPezKdq1ye*cBDQX1*cR7OT3iOt+0!@*?u5DuS_Vjm_vHs7BUL#2Qn z97OIgytP&nTsS6376=S@I=mhO*LfKuQyw&-#qQxNf*7W%ky=z(%#rJ;W7AG~X=m)5 zjzO3lcY~~wQtM{c%Txbkt;b|UbjJY*mu0-C*;0|?RiovcLlY0gsdkq-y5bKWbj}M; z5_CjS1u4P(Z^SuHZYGfpzQ^W*VuT<>Z};&XmGbL7dKp}a1WBxU>OCR(Uyig6{5>BU z<)h_C82y)j{VF_0F5m#a9_L}%7BVH)DLm%GF{QL3EoTLtq9|;)ysW%hN(pA6TYW5Y zW-b%IIlf@5ah0ppZ`*7DZ>png=Hg{Y7)(KU;fQ^>>UCrxBVSGU#zc0KdvY}3TRl1 zhKOP6zUU%IRaQbB{-?z2`{_8YaoiYNVSnkS1w79aJ7+5Dzgzw0wJ5fIxjFb7b2}*l z(=woh0`;iLwPtPJQD35z-IZCYcr2%jnr@o5zX(-V?Xx;8);;kw+>|4bLYe^zyW5gH z*u6m@h|n>DH@rJ=6ZN}$YGJ0VnoN=TT^ zsAAA8HI=#}twpguTMHvjawSKi#k_P6f%P%lG3utsbFGZ4897Clk_LxrW;db-*A!9g z5G%9U36?0sSwp(8i6bh49yxb5k0Yqw6V}_33&j8W9qS6*kMwsKy5VEBHOxD_Q3& z@8iq{pPh1ke==Ld3lyOjE3tmjeG)@)_W%r^NRLIP<6o2gC@NkFYa65fw(NPst?W>n zaaC45(ib7)gN4R$1ogHf53Eq+Op0B;ZB?MQ&;w;@2`PPVv?vh{-R`GH(vX3mZ5s`R z?7Cw+ye%1ks@CbUu^n)#s;QaeAlH{;CqnCR@%+KU&f~FfgF9`y6fg z8ABC?Nw4#u!v4Q6e}v;mg<{8v-m|UXz^(e`*BR+fs3__z{B6s!k3|?6L^5g*w9)~Lx(=U%(VaV5T{WL7Wwv8f1cRv} zFxXe@X|7H&qHou-2fv?68cPbZj$8HgzE|F#dBw-R<1$0;|GW^AfID%{qR72{ZL2rs z`Wn*}3cWy$$+&>5c3|dtnc^Q+rF6X3Z#9XpvK0_Tvb%X7N z-%ztPhSud1I%-_pCqJQt-$R3(JTZ_v+p(xjzJC!`W!KNTp!owevhIx7$02MpD1s~| zkJSv?`84;!_`%K4kA|7CiS92V_2X^7-=Ud9e|e~h?0~b8NGvA_y4KAQp}<)6cz;7o z<_3zeX+SzY^3V3@1y^Zlw6IMA3nE3W-1LFRmf%o6yVEaAIt?VUq6p%Plh5^TA62zW z9tXNc^qt${{$rVU(zbr6GUm~HatZ+L1-#4NDGTcLl=8SQPYWT3P4zmGF1$sibB`R% zIYtGZYpry!L-+7mcM%F#ei-yt=D%5j{F*Y6Ud4u+epC=+w)(vtHmMrOH+mdS6r2z$L1=CM>s$(u9%@hI9(GoA5BmTtI< zNhW+#OINAO)V#LSqwgG+>V=8`e662d#{ms&k_2K2`6dnA1Zd6I_e4eeqbtrGaoybz zs|P;r7J3~%KIJ5OBYTJFr5h4HPTa)1D`NCl{3PA*3k#HV5_+YsrN%v#5fOSg)4Zsl z8V9U0jDFG1nTckl-nBa|k7aW16yAa909Y1VJz*MvIw(o9J`vmx$CXDx3h}(7xo(_A zY7ex3Qz`Px`=Ohqe_TU)wDODf>^hG%nrS`3S>L-EToo>d2ISiI)N0b)BDD0iIMK~t zp9;Z}kPM%S`(rIGA#Gk4Xh;CYEN|cE6CG2}>SBs>(8c15@Cld~zpS_MY|XT?$Bg zDhut6Lckg2bWv)rDw{f2_@c;?CSOsSgIFPt2VYLT=2U>XE+8e0U% zkNEV5Rh~!70^k>jgRS6NKC@=)S9vdy0mROv+OSrPoNc&393O2X?Px+AH{5Dad(ifI zY~m79*mbtqCFKK;Yt8spUaR8TNEalm#Z~n@1Lx*#xJ>&i=F~3tQqzqs8XL0ymgVc= zuEq35W|`jRL5%Fjql7>{+n62eZfS%T$Rj>8cedX3v55Ultrj|J+PV3XX2#xAX6~z2 zNU!0;$_%; z2}6*W+xYo;o@lI*HYBj0Gc)E*Vcf5B+r)trE26vMEy=7^G^UfE>Uzj}Y9*eRydtQP zTpYzNi>$uHnYQLQ0PavOh>6vGP{9Mk3{tA}G|X|JxubT*Mn*xgV`F8v+n0A!&6b?? zerrF(sWTfi{$m{dvpy_sht#trnf!Z>Pgl?|ll;^@{LG)I#w3c%sT98O_Kse(5Eei>s)%*RN3;8L+s*{5HeHSH>LP~PFEADZ{7_0Au#(aY)ud8oToYx-yycYx7vX+TD;d4cD}miVpaF(FObiXxA;dqB zt;r?hJ^EmxTAGt-fovh09LLc!Ji=J~w9kp<3C_2S#(C`@T=lPiP#|Qy*K5USEq04G zf6m2|DyJV(pN|adjpAXDlU{8zYb)fYLw>Y*8w+ErmK^+j8&SqVgtv!c`&@t#)q6s;zWyTW?1)4Zlv~f%9%nm2}GlUrE_MZ*gYYkwIZoK#w%1SIHI!rOtH6nYV zeKLrWv8wdY&QphEj(VeRs$^9b06P(k7;>UTon8CG^j~a+$m!{Fw}U__?fZ(3*f+W4 zo1jvX{h?!cr%rQRXwyY{`+9CJS{fPSLGsw?pdCUPb+khtC?@t9iT-;9>;{ZJ#3Y@p zdy5(zxN_P?XBWqti%u*slJ>-kX)=H?eo$=kQOim(jzQf~diA?^@bb!@3riFTiYbkz zkRTr0eD22ELh0C6gU&mw_#Faakt3 zSEf}Ykh#A9VdRxYV&S*Mu;?m7AqaTWS9|acG3tU}n4_F~L$(mY*_f%H9fM+d%x5VU z1>zC>MGZJus9i^VK0-=kYJgs=bv0QpVxYJ$rB|G&*u{q5w8Ardpx?ta$BHsxI-Zs> zWt14}<`xlMQ6byKo)@?=3_4}K)e7ncw*&ADj8Gml>Tqjb7!^Q}PEe_U5aeg2V85RK z;P;jVND+78uC$f5qP4T5m6^+Zdbnm=8At|z37)mvZO0jQ&fvi}ndMf#t}rwyXAR=G zH}pgD>a!i$zv>->`4TDq#V1&h5lG`|M*Uk&sJcI!Pq0+kB0m53^ZvfY3A&9Cpcf7eV(g?AzzuoeN3%=91IREmnU)V-21#@)eHFf=Li1>?u?iZx% z+@H+bz}Ld%+ds2c%3aHUUXX$>VrQbNDzT)12_biTxMXtR>t))3M&~H@H?H?q~b9MANFu}Q%O}XBg&v^~vK#d*PlR}fEkk$F^o_87z?_a1$Q6k6oQnQ?ghunZgOu!4V($&a_ zR9>NI#gGLP%GUHUF3V0OMB`l?35NPLD$*=+9KcBiWBO!EjmKN>!UYE5 zDg1dxAs?%5cGB%#rQvm$;K~iBuS>m6*}weu9-VHw*~&GH@*$p|>qYRFV4-5SFPvzZ zj$?ofIQ5a}`**4ukwPFkhHi$les;n$eGa4b>wGzVMWEuWA@$hwiK<=rR@_&%%LXcU zeDIQj%>r!a?)|+ih0vQc8q5Gd}`8;XlFml~@?bFo^%cl`;<^m6!^gs9bykG9nU zr!h6jflyvdKoG?Rfj;y=w^A4iA@$u%%((6Me#QX8ma?4X#<$p}MUH!elHpyvLf8mD zXQ-x;raC2_i`)96g2c3Ivu(n#ze*{xTa4or3X10F8wo**>pRpG6W#y)BY_IF-hb_q z4?W&lG_W!=iaGiFEBe~oJDS!h$Z;@cQ4eg{U}eIA*1xdv))k8U8|>?QA5l;NebR1) zUA6z+OTa!utB)BlfDnRPV@8I#+6t^$au^Gh=1^y4BXLs(NEf|m$wTZikn~?!7BJgR z-#8tdn!-$WG(``1IOr2${ZAn13z_fzRzO^@68!CX%mW2GdcQmu`09!I{lZ??qD}i$ zGgYntZxxp@a3)1_!F_Vdc7((EN3R*6fha=sjeU-LUX^M1MMKWu-0 zG&|~GX)x466b}qv>L0rXRSKTLzM3w)x8lcqfFxagn58BteMB zxSO9DX8|~7xI_|@dRwSLHyB5%5lr}3Wn*Q9tDd89(wKvc9|vOGU;7T3FSM>mi=|BJ zQ5R6rRmxuh)+BqP!0L^OL(&US(3I{7U;_UHL&D0wGxNPX)Bic!9EUe5Awgc1d_N!~ z`Te&X_R(Pbs$NDpGRjVvx*xF%vHAWF;xMpZyR4(BNeRZ=Sv|i-M`v5HItTOZCVmrI zFd1?Z4qT5#AZjcjil%cyTjR;#>HD!vMj-`(_F%ZPL(~UG5Cld%YZ>Yk>uzY-IbrRa z6=}_F|4FUYeW;2qWAFJyNqj<{>^MO%=`lR2D2`?Ri?z6Gy5l<$I|_b8J`5TNRNFm_ zfYM6*eRu8NZ{MiLYV>h64vu@>X$9bdEsWrXqRhRK7Ks+lTqnDZvka_OaNq?oL{TWB z7#ZMq;mAq#8eO*VfDD0{KE}%z-|&bfN(DJ>A%yj_5S+nJOyFY8G#3Dqj_T?5I6RP! zUBfUf$Q(HL39q8Z+yLlQNnuCeRAeDf_sFDM%ckO?)Tr;1&IzDipSYW6`oFK& zy#sk!;d#992-*F#{La<;D(ru&3$+$!b<0F`=^$3C2jXS6aN6T zTghLlXE?x9N_7TLUF*`2tD|wI0dTy}$Mumh$MVmiw*-ahFJW`qm2Wu5~@`M^{ zcOoowd3@EPfaaUxa@c<*Wz1$GNQb*#0M)M@RCXNz*sTS}vL_!405xA~iu5FtcE$o; zjIq*1j2(5tkC4C?(#YOv^3s*1i5EygH2bArmFn;%Kq;TD5hPo9CDHI?8CDtvUr=}| zgZlC1tgH9?(L(^lIrAOLmbH&7JOnM66b_5rmwA<1FI<*p$lPVDdWA^*6jQ&&9<0A? z8QOOC?lf_~1!J{)l}RURE5_Q*V6c01rLGbuXFZ$$BLZG74S6V= z6r9ESAN?6V&kOVw1-}I0C7elrQdqo(e5Ntkf9=mm zT2C~gT{c{ln9x|8(N;=BvqmWCdV{^rYU~H%%cOYqdm3&1 zcI9~P_Zx+cqik=NuH+N`%8W)4@>7zF@Vz3O*9?z%Ibi?&dPrqLJd1`+u}WIq#xMLOGWH@#XVAq->oS_j4a17M(KECLZ_sC*h*awCWS zK~@@j!&6L(<_amGK@b!5m*-KupQcT~{+6wgV5&4W@zD#@!7YLk|!)mYxegJ0@kBQh1;CZg-m<|D~>cfU7Mva4Mskw*r=l;E`Hc+Jp zPXbN*zI?EEP}JP4>(~SV7#JKz)?s@Avmd(Y2@-I_B0m45IWGi|RjxT#lu7c78(Xua zN~wm-bK=={aVSQj@(n6dz`U)}e7+>_kGej-M&$fc%Q0Pq zf;$!FFi8Xz+Ygw{>aydadTV_xoeS|!;(eF@5Yc{UMhvN1z*1x!)dYuEBcqoVu&8iY z9M|2t!dzL!BY*CB=^}Uoz(L&DJEu*$v~-Ls8%OgKlClOYWEeKD&*=l-OdS=b3t*2= z#&P?^*GIh2rCE1b$P=IAyhg20&UAe6BJDb`c?*W)7Fg6TKQme?q6TWk;7^u_3Rgb0 zA$}%*`==ru@dD#i6l>&u2McBg1}&$S;K4|n3c29SFC{?psc3mJx&6@ z_CsNkx&@Qx=0NbO;Ql5e5{c_fGUeCv@JGy|sjTTe52kF4ucnQs)|2@a^on8?O1WOd z=aSxaC_Jz9D{%0E{C!=rhxX*aXK?H5H623%;ITTT#4<6^7uNmLmstYhR#(A;#$Ny* z_z(zpc!MVOBg$D7wfn@Qvb`Y^6Dq&xRv5qJ@BZ##qf57xJR3&PMUt@fns&nVmkl%; zcf42r&kG7Za1LonpZn#&V?JEBXD9H;_tEV#!_fz6@k7wZ!gymevo22J#P`<)xZ?3X zm|SA}aL2;SF`|L*#LcE&WoYyw0!(2`$AdtKpe+I5VmDR#jk)ggS{e*_?wXHyAO+C< z`AH0(-cp#?V6|4$F`YHC-%iSkR{zOul6)nTk&dT1#OPBiGG$Ezc+=*=XA4C>~ z3ub!&p&1TXlI7skIg9g3*@Y}V34+I?%mXg93eF>*EB{w}MHE1c_GQ8#S_yg*a7NK! z*g;=xX{qRr?vdbuFiQYok5sl6xfkj^i-FXx3Rp1q61o)4EJcXrlW+xbyy*YT;hp(y z(rC=vF6x-R6P<(BIPn$OJfn##L&H~5?a8FLKI;T7@EiQl)B706NrUDpbxI@s7nrpl zMn)Jxw!R#8EycjX(!VZ5#=g{e^YfoCv~e^?MQjV*8~po<^NH=~HQS%qDJ%#ah~tN$ z5mLyBi=tA}hhft$oz_44$uQMBf+1%+3Ny3reE$t8YEc5Wqdi|qMX0aO0v>*juLx^` zwL2jvoajaCGYKjssmI!Q4r2i-O)NN zH_tk<($Ofvq8UGfmkBS~urou}jt5IZlhQFuw*pMiNFb`g4ONXi*{{>bjSCt_)4x^N z5!XA~H)%L}@a4R^n3%BImz0JJ_DWt-FyXVtLI}RHXvEsSW+#OlenMd1wCqHZ?0RMQ zAozA56wsY#dwHwMrlO?RVV1hg5-%%diFf7Q2q?SZgHfR{c9WZ(-lF5;0FWL^%?TS{<$n2k+psJgpB9P_ zfH?O!Vye9!j7|~Mdf92MIMNR2=y0y&In-CeSRyV_B$P$mNiA5PvOyU{w0I*!M$TmN zFaZRdXeJJ%@W)3NjO9-lNafF74SuBWP*{Oavo8{Xeo2#lH$r8NqhqtPil&W1b39dz zt;7>_Z9j`qC=#EP-2OhQHyFm**Zo?g1iBWP5mOjyn-tV^#utddP`G*>Zuy>9fV`_v zrQ?SW{In{$72uN;Gx@(PUF&j!W9B&ZYwsVqk_j6{Rrkajr5`o}r+(lTJ3XL$^?*z2 zm++#sav+;91!uCC^wS6EA)f36L#F23^{~9|?uLd;ujK*gj?OeK$7<7lh?W4}TQukBm~ zft*~nuJ(({b=ZJ5Ue^SVl=F*wp~`*Hvan2aC?0^_N5;_Ny!z19Yu8Hr#*L*FEx7A? z(z9Y55+MM7g&g*gaExW#FQxx6%lF6rA#PYxCP=DwfKM6tCA+EZ1CXFg-SlnzXWS`l zI-!oE4Fqm})Sjt2c8Dbt5p#_1rR3v7#GzF*t1B*83@9{qOy@IC$P>d@Utb?{P=UJa zBrziX705}Htz$zCoLZEFTU|a@H&ZT07tsbkLQ(>piQZFiDZti*)_VPu+pblRaepFv z(xLT$*XMv6oemq!)Y*dVM2|dME$n_Rj9hq>`i|eikkTfwLwiuZL22 zqW4bD8Y`#&o(aI+QC5~4)eL@lzgE>aSY#%)MrpN@T#Pl5vV zc^KiP03UTdaQA`%T(0fx>n~KmKK9Wwyp#I;SJLWXAfA8LJu|rr%+VY;(qF27hWs<% zjb#muJ{L+Q1rrBK!Vp1CQp}E@sj_sjY%|)QP;7u44!pGt+w|?}4CowfBXmJ2y@P=$rcjm$()agRd0QSR#h|cyIJeJe^tN;an(V6oWQ8Ts2*L%d^wtCFHRm zh!|z~yoDZMM)I||00=gakw}Z81(chb^!r#G8c$&2 z@H{Bg|^uLd>(83il6~}QBp1@c@d5WtI7f?P+K-0 zin;H^OGC3aG&ZKa0xNU*%RM#|zjYpQw%(!KV7N~WMOOZ6zV=WZJrA0ts|-o<4S9E7 zx97DB%F7pUd@T_Z5k6F%J$(sQ4aq+MY_Q<_PJtA?^>z0L5^&LS(<)bp{YAL8=mG_v z42(0|E-aF6={TKUu*mI;U-~++GTt0+Gs+Y6Z1lWvvMIQr2eXH0@Hmm)=M6r~s!-Ln zKc$j56rkKB)z**cj}9*$opa}_FmPLYJ=UnKf+>LRUJm1+i~CJ>z1{ip5fc(7{_*@u zob*1PLLMQIdax4?Du-J`Od72zCGk=}N%_n=VjbxNsWm&_-bS}^l{57Os@-gT{wA9oX>cNzeJhSlw+@lyQv z0+;?yKMggQuS4`YGu5gb)bIT{!Ln9FX*aTyX5u41yzN zFxN3qLi>;6%!K(tP@EmKcz%R&@|^#V41VGzkrw!!nx1O*r@61HD+*bc39|k7ZiAn= zD|;}4O5{jVx^>wn2FdjufDo;(=jb@hGnU;1Ub4(b8WuL7`X4#!pBme9qc;wD&0ocf zgn|PcQKGy@GDn+Iyz^EV^nU4H0^fKwfu}r7BR)X{hPS^J!S(Yljm6NDipIqMXSKoS zuw|A(;_UJ8Y^RnlOGYJKua1;2t(#W7uL>*vt|Ia9XKuJ_Ni zxEqX{G8m7HIhp|cZM?|I>!YpwRI7pHCdG)kvC_x#*GIlcBt@!IaCrrx_}3Ucc&gez zyBNbkLiETJL6pg$)5TP{*aExq>08!^jBy8{3!K+rn4|u}db!QG@%y~s8P1W$%$(4# zMU!7nU$&?|-T2_vH0Ztd+}^j9Z8#Tx`5w3g#zwzIv2tpa&17%JZsMqy6O{}5m^ZA-9j&Vqf9)%biFKcDPHiKp>~D#95d|%|du|Gw=5YiG*OCr!2z}*10z*sO z9M3x;67Gg%@QR`JiJyejdlyB@UYLJAqJ8jT&ONt?^7O>6|E1$2f|#0@4ncd|W;cz2U?OcHOQNkL&5CnM66p9e-#_TbiR3h#^~NW1vC4Eg0cpmlnwRB#Cc0G|QM;}CI1 zGiBUd%wop~Amnd-Ij+3KZ=crv(H>Gn{%Ju3KOb>X>rc`aUB3pgLykQx;N2$i@iQg; zgUaP|#DM$7Mp(1yxKajJx%PCHP7~LbCV}Aw@3|N``PO@9;(n0vczdl%A0ro0>XaMz z=hvr(rrs7P2jXR7T%6D#KWz>n>L!K{(iS@Xi{COc5-7O7Qn7rxLHEjBz!3r?qu>~4 zTJ;<`IXKm{xQ!gsxsfU0@;vVer4q69N^a*WBeq;Fe|1Uj3}sUI9xafW+|YZAfM(e9 zS;rZ#p8SDC+#f?T>gZjd8`nsfE7XMG-ShuR{}@r-l7#=hX>)UF`Ov* z*rfzf8necV;kde*TGWb@P8!jc3-WoT4hCIcjtlxGs5P4r7(AazPf@uP$Olb&9o{$0 zDSM@5?KBh6A=KpuyRB|fvG04aIAD?v=M5__S-o03opWy7uUC&ec+fyjFe-Xvl6V{8 z-J<~K_d@^mhJx4@hERGZf6jSbpTXP=!7#pDIKZW1El%X=sd@Q7H|gQL9lzUwm*bvB zKf@yVvmDL5sDPhtv>Y0PR>{?#y+jrPafAeeNHVWFWzW+?dKc&3t~+2SpC4xjcy%9; zJ@@_xwE555i(&ITV)P{f35XtWQJB*Wc^XTe!xa4eA*4Q!43C31 zouxKw8$9nRnC4BQ_hTttwm4&dx$Kky&CA(SfVp5YUJaCZ8a!R^d2JLi}pOyfYq zt4zXDBkC*d}~)i{$33BM#Te&6dz6ZgQ#SJ}Y2GpQ`tS2y()7S*;mAo#$1C9s~% z*_Dy$J^pv0683Sm)_2d2SkMzVTKCHKdX3m$V=NLaqvSoSd6vXljSU@MyigN(?x;tE zgoUWHNP0)3+!*WoOuuJ}YdpVgU;-pF&D@f4$UcYa1fxC{&DM~wxXZP&AW}_6krPM% z4vS=Wr2yP{CR(vWtN9w{%%n_Bzk|mIUcAU17f5Uklqb_=EXdZyzWDvc{3leV@fy>} zF3LH()N)u{}6xB^XZs!$|BMbT{NFAbO9YBVs7Sz*eg@c zb33hE@s2+CSO!R%p(Bl13R4YgzO|>7^x(D^E&EZZ?&z_DUNjx~a>KgOV~(+g+>~ud zf((c4|G4e%YYV%=(t1yK-`pnF)>Mj z*gbIJDq>C^Sgm6Vx4#TtMya}DEZrZk`Thg zOY#09sT__7j`t{)AWHB8*d>p7;E)uu=!d;7>?Bj^_o-gmN_nUojG_q+Iv*+w7>Xe` zuV;eGn%WBZ7v5h?Z!uTY_AKISHJKR`LFxx5^71=?;lsfN(s_|c{uhK@YdVGxqbTtS z^gElsAr31GVLR_6uyPm3p$LCDU%A4z?=G~KmdT2h+1a^aSS z3$sO(IQl;~h=&UQ)NcYqqv&Ds%?T!2GcYKq-+m<(3S_i4A4>@Uv z>&L}G_3AZc5H=Zlgo-*o2LDKDcx2^d(s%U00lAw^G22O5YAb~)QxyfR@Sj5fo%ZBUqSCQSb%3pR~7tPG8ZN9{HGt73zq{_Ii zK8x#ZO5PNdDEYX!5k60gzjmSak(1WtO=) z*Ge55{?|(WLnp6%mW{aR$5pytD=^+6bh7u^0BU(8$C9AfDt-ilRc6Iqu}y{+TKh<^5$|`)MLfWAj7;vec}H&`U8rHT6Yt3i@*yE&(*ex0EB2K2E4beFG;|WRx0jiHsa(;`0-A!bXd-lA$?VUrI8qfRZ zPpu@Z&b(jrwc~Vrh_H&<7sCFPKApk8?d*~=?FK-fo-SK8K#LdA<}%xRVirK9qCExs zK8e$FZ3R{;$e85_THu8uJMGKhWBDCcJ5n@4AV3P4f|CqSWk<%8qWL2O(5HU)JJLRW zHo?Dtcu2@jkMO!YV!Nrkt6GcYmUD6<^}j93-_9&ejR}0`!>^iS-y3W@)=fMPwoBM4 zWhIG>i3k9TFeX-}?kc$!!TNw$&RwwST={;Ylo_aeH-tHX8R}HaA%!KB@E!)jSic!c zB2V(wrSO0_rR>K1=rZ`XQkQUn*larA-hPqR@)PpjW%IvIK5)68Ev1v~xV+ZS;CJMI z-qYU@`}pxOj$vZ_K^MXI-1X|35eG6^BM=l!yn=Y(Hu%cP7%Jj*4qWP0yv4*+q*oaP z5tFj=p3igCDwifmpxkfAhpXc zn+Js2%&pj=f!_tL(E6@P^-}uaulHF!nMi~Z(L#ukBs$q*C&tF({lZ!Afj=BY++?rY z<;7XPh;TMe0X4g73jOoxm!jMIb~lsQUbatQvh+p(U)i^K&oPEcl>xs6C28&DA$T>g z%9;neK+8TWBA9ehnFTZ#D1UIiLJ?$_qeT$9z${#H&#Z z$+8rTj+Q0`WGW)(^gK?w9@7yoeCTBSfsUvB0j9Rf$}Il3t15}T7Wsyu}bMRsi}{@%5yP z_d^_)goMQHNP1^vM~9Kr(LLU>eP&^yneS8fOU0qAL`m06yB8V-yRf%ITgzXx##5bn zcnZaESDvd|4rqr;ATmS$#|jCWKqY3f&GU%u3g$!{f$&@hjE{0iaep5$qc=5&-Jl5n z7S_xCnV>;O0kc(c83<|kNmOb04gUrkgIb~}yTi3XM@RnG! zjfaMY+sc0+7}VD7yt)yJG=90jAF~G=_IP?&zxwaPH(0mQ%Jt{1YS=rhdoB*>loc?J z`vXzZri8c-#=bbyJ4NLYPr+q$`uZK zm;IijX6a0f2ne~}ddcB0U34dZ({Vj=I078y};>YZGYcUcaYpGtazj62<_ zbA~O^A-XM2qu20>S26{>L-)@mHtT%WV+qNoVs1Le%bGfGip}O8)fLOtjbrnibQ0Q_ zl3;cZL<(H_@S+TaW(_bK`K^c4Acz-wuDnCE$g+Li6J<%~K4aj~+hyc+2-5nsq{}YKIR)QPEEYU$`XzA2z zKaTLWRKVJ9dy5PzYjbJ|17wljJFUIdUSu_zZUX=@vPxSSYimuz-zO(M;h#_GKNQXU zc3FFEe>MsV$BHfY)c&eCmMG;1X{Fc8@KQ%@!yL!$TA%)TI>3EPlo#HQ9(rQj>2q&!uxJ- zM9G~832cE+=;LG%)hXrqd`BO9driNkI$L<^sB(Hp*AnUd#`DRInwlP-R_BjCcnqO% z43B_7Fh!mA0R#QUq`yra*Mg?}PZKNoRw*lKS3z)E`2pBN@nhJY>+Xev_XOUx}Y; ze+^nR=sqkW0>vL6J!~~1_YnihYppG>>50{g5L*2R2IEQ08w2*4USe}{Ru7Gczx=BG zlu5Z&{~%jq*ZY|s+`Gq%c)uPhZ80Nw`upuP21JMD!_m7r1tpbR=X?hnOM-b_iZcFb zTIb7Snw^h8 zn5M1oC(Sz#920Z9`7@!&>CCT^eeG3^Ft9Bji#V8VRkOy4%(R~tn}L&%WFgr*M)^3C z%Xz*gGtmLFE*Vh4zKlvUy_O5Z{leP$$nkLJ6kKpe9B7>%*4!3!3*p~pWyOSc{r*Hr zLvw+10QwCken3o?E>gd@%f7W(f819&D!ZpZDkI5?|5I};U!^67l_5G?o&flr+5c7nLuO*&2hCqP)EW00Z5;@7WSh@ z&;j|zlI}kd@ATxr16uU69@6$5-6sR^P87SwgY$a)Rbc%T1dw7*6KHIY0PTTzt{5qw zo+`_8iR+n)Ea%s~-al{XZ#5yijGUQ+6AO&|+piak9CGg(n z{bf`TB`6`YTDgi{L~EYnueX|%221h!r~g9Of49V&k4xOH=?^aThP;@FzNwY3HrkQ| zF4;wyR1kJ!P47YR1)0?e`>&P`*M*FV7ZDi)C{S2Rr0W<@BJviGtV-@UiqpUILcLBh z6cbWeV-Y_<%qxUBA)$wr>qq){G1t3wC4GZMX;5!FbDcc>_iS5LJ`+)tXcB%i))YP0 z>Vym#Q2V`sh0zxV)FOuB>uFRYAt8}RCr;9T6*9cXU z8Y(j&tz;tpu4HZg{hb(zs;fWzFXwZoTh*zGN|ExQLtAVv%~5{7@4tRh3b`R%`o(ov04#zxfxiC$v z6ovkZn1AmSz~rqH^~K{YkpR#*k>M!hH1&OMHntby;yw`>TxTRtZQ8ypTqy+(3=pW8 z5p(w5(E$IvhLwTIym9Rk3m-VBcbPV`S_vs#N)=!VN{Ic3ntMmInO2o-7;H4}yjN=z zj3zf3g+D`n?ka_s#Q%6#_*zO1ZhwyUD42DYqOe!E}*duwu<+!W;sok}Iixs0W~ z31x_Se2xNN}N8GmUmtg|DIBO_`N5BXJJ z4*|2m8ScUEpcwx5+Ykh)ezkU>xY^2lXX44unCcC}CcL)a%<5iWkL@umgD=O^as#F0 z4dKCJhwaSCAs0y$)ixt2vN;KS#G@fmem*!50VQ)2gPPh}>?`PesdKd}Ymmp|V)(OL zymZVF@@8|ePm^>*4F(f`6q^i?CE)e{8hgvQDxW8A_@a@JK1hRzfOK~Xf+(OMUDEvn zq`O;6X{1ZKJEf5p={|>)?mXu_7ytY7+^_HR>b%%%cXsAGGdsI8yR7t-K&@HQi&i$26N*56}q0OWKART!k!E_~cKkN*q z18mnUfY;a7DPvOaVF#$K-@3<{PB_U&O6_Pm%x0(^p+VhspE7u4&m`}lI1*E&2t|Ci zceC)8DAWgh-Y??>;Ab=W^Ho)&oV_krGO#{{&O{H71Z;M1j(TT*RJrjm0DQHqNHtTGZ|pX zfGp{|bMW^?>5!1H5tAt5Nqhg^E%7%wd~J1c_N#JRuV{}gqy!NqD0$g(a*y+7 z4e6_ZySfa~j9&9o?D-8=_sXdSjCumfG~zhjZ^iGzeXaH^q@dsA!ScTI2wN0DI*eXC z-%Q3^Pt4Hk)UyFjTUU((bRf>9#vacyF0WHd*CeAA=r@1k>osMmc_S0#-YRsfAG$TH zY&kw3KVp=~DWCEWdQH{zpF~09gzocI3_#Fs@vbXG_M`YsQy0RhzCPp`X1Yxm^=3Z14K2Y6BmfQ zTE{kG=T{>)XVY=7lg*h%g!4nxqUd2EZ#rTD09z-Qj@*g{f=UW9-Y6WxuVu%vGwro!o#&L_U%ZdOK);aC)C1GJBYS>LgAcN$?SzKN z>5yxkOPa{{fPPuqU2wTgVVDwU-Fpr3GM0K{ICttzB!C@48h3#Kd{|GRYP6%{vlQEJ zJP^hqEUGD@>EM)n7V(|%Ks;wx+tC*^LIEJ66Toy0fH3dQ3m8pf0O;TGc0TkoPx=(S zwZA4wU(KqXI7_3?ky>dszkeM8gLD~7KQSvZ1(Pa@mq0yH@qsyFIo;shpa1x9#@ygL z`|1fZ0KVY4A~$QGKqiSU_py&8PCQ^-Me@ZfT^Cngrt_Hg~oqKR$M`d zzgb1HDV<(|72v+ghwze6_Ms(hd)5CHHY|8bPvOPyfH`BHbO+J#U{(+a;spdD%F|Z{ zs~`B(C1=+EHmnAFo0~%&~HfiuKK_=#4mSyMc)@UE35rt`Ox*)MPZ!bXMX;{{l!jq9 zQ>Nigum-L5Zj>zY9Q5$s*edaJdGV_gv}X}=H`$X?I(D6*pz=gnh{)(amqkhfMJ+#R zd^wxj^(VhTv6KQUyh-5;U7+9muYa)16n|w`>bazq&7QDiWnO_YX5~xZW2+&lyq!Lh zbD5UhVj`>T%8fr}>2QDm0T;WOB0$irtPpu>-m@~q*)6xj_nB4Q&LaiMa|#RPE7{r7 z2!d=N_uv@>cw6LOw~St6sK*GQpzqI|sE8reZ0rAaVhrAVelG`%*Zg;Wg`9rkA-=2# z6p~gUF3p#}zyySS?+O-|rl_BW(@glmY@1IP*{U0ya5p1<+)hJ2v^v7J?R~6zu|EXb zeQuEO3IiPvG<=`~$M6NV3g~~o-#Qqx^A$Z}cBvlTCF4Z?gpU~j!otE%c7v}smXLS& zSyVtyR~LcN{cpbJy}Pm+=fTE^7@Z<||EC{&FYsf;B-Om*P6Ozp!5nyI)GyEwhGIG_ z)ml0RVkN&oo_RucYSQo?l2E31|0@4{JbNXr;EHF${mc<8=XSi$y#Coyn0jsqvv}CM zMus;wLM9^+EeSG&cPC+EOSb?)D7(W?TeJ_OCie$uDGX90VDcpOIFl#5gYV`(7NY4~ z#S@a()a0;}R*Cnu1$?1f5Hv$YvH$+baF=p^%>97vf3xD?1fMB8ZR+YQxN6Lc?KK{Z z^S_{LzWPmebJXQWQc;oMcVn7KZ^G+;6F&Avv)6PC^iorTzv91E+>>q*b#&7q4!$mc zT~``4bFWnl8semrM^4n~Rx~39^uQUoOA}t$IB-1&-_2`B{4+WoUt4=)>FwRjO3KI( z47H`AZ1Oicgb(nIr2JjM!6{nU`95gAQ6r!Zrl<9`fLoKU8>+|DmW|}tX~XYA7Qi|S z0Lb!%+|eA**rPe>6iBL9O&fzZ*?H_v4SvubB2AbfmCPon>OA8WbkL2vx)S^#;-z4N zE^M3_QntF-dYQO*AM37Jo|^sowJgCiM58iz%Z|4Vk&CDw)w&gdQlJC;pD$B|ztnuS zxp&{uhA9dn#h;cfc@+6HDYPn3?yA+y%)-Q}m$QuPqdOguvM?+PV#z~ug7aqlOmveuIM(;a7OQHhAh2&+y4 z<7g4I#B_hJ#InGW9=`IQWf^Q#L!zkZlzS5|0p8kL9{T}2edo)|RBr2UHurd;vh(*- z;q#4$64avAbiN%D@y`=GCIYb6{mI!^5q)(NL8?WO+~6+eLqLE}+OV4&&n!D#@gp>L zJdSRCg;#rgeymaD-SJA^s#53!`T=)$UUeu?Dtc|&vEwN!yrJPS88A(7G}|b=<$U{> zU96p^U?H!rp049bw4-^gcwU|Ca@)Q={kyB zY+`&Ukb`;meZV%L$=v<7+n20t`Mt^2OXA!2?+HmsF{7jGW@hFL%wkk6EuQp20i)&Y zgtCMuSnJgUy@#jZZc5Mo7vp)JkU)ft@By-`!iHA5n-#A!v+v3f$3lBNjsahi%+w57diLKt!C+cGl4Gg%k z>&?|v_SHbkHp?oL+4a7;W=w%^{_V6MBNFq6_ORQpV zIx;vW>&h4KT4tw?(5gGIyK(;8XoG7E$MxA39T1ag7)~oPCZ^8f{}*o2J>mDFLjU$K z$Jy{_Gy3{RS64lu0QVK&YpkO{7y?~af8sVkYq~ok%){l2=w%ZP4NZGJvve4nyI7Ii z#zs$7R<8B?3A2y2FDP9WNxwvXf+%CbeT)caT$PmvgU7vdp?CqR4Gqcy4KWqA=*w}k z`<*%GPqG{_TD3%E!b@c*)b33 zwHydiNtWeIaw3%C-eul&v^RD3o%G}~(?@NVeWVBwMEs&#cIrs_wuafKkVAwkTD4Fl zP*0d=WMvom>*hZ5RCw)~r%IbiF?W6O?C(Ku&gZsla*Piu(!Z?49U*DuFwb_(PFk07 zTpF#Ko$l~sueEFzCIlP$s%K#nKIU7rMpEIY719^0NzM+Towy(>mRERRf*tLlG7kq(@9cO1GO zp$nM25$k-YqQ(sn`K0p%%L@K9;27vOa9=q!R38p+AUwvnpp=$FeW%h*Mww43{A5>9 z>*pap{^5NvB6G@)L<~4A0fb#_@UmS5-WxuZI|pg~X-#)R!s|E_6Jj?U-P{m`>+jn?56?#3+lGPXD6y_;AhOqNeCMd4xtT-x6HY4NDN%OyGvG= zK}uR3Ei78aU|Ujm&!kXg{1)Clpy;g-O2Nz&3{`{$)+rX$uA=dG8fwh!F$S#Sy@QLp zOh{ZW=kW10VXOZ~_F?!(vlQekwbcvycy#WHP!&a{f%*X;MDjnzFJH+` z34vwbxozg0{$Rra#F1EKza_%abdptFv4Dd-PJA&e?Q7GXKgthPv*p^Wt1Ez$Psc|X zEj}E4q<0+kWCkx|cZ`L}j{$@Bg7<1@@1u63?AKxxE84#jYsa@+1c1`6!`YUU_f6Fg ztAvdA1^Xx-qK(!IID@U8J>RztdF}K6>&kXULi6&UBS~FWqo3dcdGbeF<6reUVx=Zs z8Fmv{(QW*ukN20Q0;&dKPPFn_^6Wr_th5pwx5_jwt7Z2(IBh7!?Dv~?)1h+@lpkE$ z>34o0js+iEqKWX-B!N)S_FKZf~y*8cd+YGCLjGH2^&h z0Z08sJKm(%jsE2ApXYf3)rAPW`nc$6fj+H;ONXOsun7_VmM6t$D>F>r-f5ZQ_tAaP z%nFly!duJ*&bLuBs#uxVCIjH0ayGCEEWpK+X3cGA`WH%G)8?yC`xpty{0nkV*xK?V z;ewP>r=l+YbDm~2ewfoqk#x^`c0;n&eOhxHYh2wyHn=_V>y6yZcR<|!R8^1DP@IR- z<#&AmlbX(f&E6$R9(o^n@VFSBHb5BqURikwo8Ju_&nqGWh1I>w=HSgMSXtfXG-zyT zWOisUA3IaTJvFT|Rn5-q8xoE){_c;qLt-U$urF*qK=zLd?27%dtI0F6va4=b(zGsG zg+en${KFLG&F!lCUtYKzCB!KhZgeK-EQH^%-!26n7^~xyw|XiH!HU%Xo@eI?2(8r+kQ@dW4cVZ8o0h_5W$)>dGMj zT!G|!bWca25V_S~e15f;ADBIfm6Bw5?J?;Yjt|0+n`G4+)rCMg_n2A??T;Q6Wtt2u zP)3SDxBCV)12HNE0pu8x-?!-j@4aY@j*o*gW#7DpS+>R9FLLEEWR+N3+2DW!J~H3r z@bJ(Y+e4Cg-su7LC9&qDwR%>RJKk+3lhUgVAJ94VMqvOShgv=$vAd@)%Jyye>C8;& zy*f(FAYxay``1+%2-VlG?>|hogp+}U#oJn0?S~IMt%6;52xBjOlqpvB++Ilk-YJTB zk|RdUhHwLCK!FQ9M-92DoXZ_fj3IA*6EvQBHZ(PD+8GzwN|{|%S-I_{763|s2n6gk zitwhZ4=gY+N4-eg*xcZueLJy>KG1R>#^D__E^+YK43Nh4nb@BXJb(M+ARAlM{5P5& z)xTJ2G#-FrM;Nh&x9nD!=S?}%sFraqSJ@%vsqB(G7WoB10pyRp88>T zI}^qd--m zr^0jLXQcmYb9rfh2tW-7rTh=XIlnqXK7;ws{9j{9B#-?kp;%l^sYT-bARAYMZm6ml zlm5)p9zPW|PB+gdv3{sswXE{bnF~AJ0;y{PElI@cCvX`c5g=FvBgmqbzb0%$88G`ZG#mFBB7MW?FSn$v>*f42SUgdCvBkF$^3)1SDS8&JcvrJT7SAvy17QF^Z-$}oBY!gBMiukR8rB{aowP?f3f=h{Sc5agRW+INr2!a0ZeRF@NaAJXtd zIuLFNmP*ZWz=^m}dy_9PoJixcxcTeiJtWRSYTJFq2<P@NfZ{%fE}-PXoWC z>?NYgVPz9ifVl3`vG2CDkl7k**JAKyg3ZIsff)yBmcjI~$%T{KFDNDi}2M z@GxbiilYVfxuIxU$P-XhiFK>1b;0KQbCqP>ZS4aW1k5LhrONQcWx{76VEdYe)$1ZD zCVojx5{G-`$=m6J4Pt=EfQX~c2z}jC=%%HH;&0cwClY)JG3ytrc*DKN(*&rkz-h99 z%#LbZG>934gu6R9ImSsucB%nK$Fq!4^h~dx`agcJW9h~AIQhCA%*?mk29NoZxFGoN z9}N(h_c4|pE!MiH*F3mf9gbD8YK+S!Pu_xJX(;%tonQq{3yw-vmZLkk>V`C91w>S8 zi$C>sUWmaGIHB-(FyY~`H-2kOx?jciL=-Ns{2HdPB)xeF~OWP{4?qjbQ5kBs~>vmJ7WYRa_Bf(RPx@$3wjGf2-reveBY@i(XLF3um zPg8&htJyv~AB^sZxl_>EO&c&blX5FiieY}dZ%4AFg?1w=B8414fv4;J0A-@%?Dd)6 zdAgPA4MQKDR}4k=cuqBaQ0;yd4oKS8`Y_SMhzio)594AsePDJ10~DnMh^eV^FaSZk z2fQEp>>LgTT6p}mrv7B?O9;heKioAV)5vlOp4V=w1I1=N0gn?AJjA}{1H{+3z_ppG zgga_fRVJIDXaabrrKfNTEwZ#%@5gCdt>KGHD9f877LLSC z9UY|o@>4m}Mvm+w{q9*3A?M`keyako9k@*gs;kxbkG=g2PBuNU=K=Ta125Ec zM$PQn&x^=pio57p@E<%Bm}4_rcEvPbiyO?%KZJtc1%V63i0x0R$KAQLwGB;A)|>u^ zWXHo9`DVYZKRMGAwz_DlN0PDdLC#IWuQpiQgZFoL!YjmTj~o%PoQE0%Fh7&5MQ2p< z0pBXi@ZZT{(^g^!Z6kDm%RILfByKSCahP5Ta5~N_XlTPAZYFTo^9@b)wel^kD33c_ z$qEQ%q-0kYrv65E9Ku^dt`-*`;2z>fW%T*|;g4d9&gu;8rJ7y4`44corsPYi|GdB{ zm~24ea_3p-ht#o)bKfCk<)oZ+pXtwVE~U0|depQrDm^L!lxG(t>Rm|o*zM|H_BA$M zPS_fk#m-NZaTz3Qi)${;CMTqC10RRT@DP`L8S?M~n6Up>kLlN1hD-8V#*jMTP~(J}-g~ zar-s0HKj<=)0WuVA;Lj^pOJum8As0HcyjdaQiztM+aFl>H0&yKr#{y97f2=>4Dt&R zanIRD^#k8kn``sz(ps87Q+(vcqv63QDt5^6?!9Yx2iRRv<|htJ@tJx|h*ioPkK)}C zZI7N0a;}4ZPQ);Q9Z}d=v*ipTeM=|{1cY>qYH`=|daG*^zNAs8!cy!njp({)``$nq z@tQi4S1C((!%}e9GB-3FWKW{j-Q8X7 z{%loA0wF>7U{N<1Wg<`b{TyHoxX`=x`Yq^vmv~UV<-nJyIn=h%AgI5Vfx*~2Vix8{ zVvjGbvNGqZ5d%-#|GpSHf4?dc+7aW22NMQ(Z3wJ4SaCF?6jhoz}Um?B{)%WQ`>VA zS65ek(Di;B6yTkLf~8T$fV8S=9C|mR78VvS08)s>i)w0M zT{t#EXSUGcmr4R#)N5Z8QhqJNO2#e8AwS&mI@XBj% zUw%eIlT=t(SlcYjKoV)9qn(<(=aTzXC;97u$sZ?!gcys$v=}9goDge;0(((4R;KrZ ziD7$+h4qej!5Q|>pIa-xsaY-Yx9f`h^XPINftg;*f-!bF9@E*Ro)H(!Y+M2{jsuzZ~on zU{5eoM0TN&qB;v8P=0E-u}Hn(x$Zl4rCx4oq2DQ@Pbwr?>K_<<{`<^nU>L#WYeNzhop+vG zmgIVRbEQH(e40uJkn8$13a&ofv@ilhqoj5K@)+Mnb#HG)o72L?o z5WRo#MJ$gi;0XpiEh6?{g3@})6}JKkl@m;086SY1hR*zpKLaDR-%0JTSTU0tZFbC@ zZvIS`uHk8)k4n+lC)bov$*vkLju>d}y&&{Zh%@`L{fdUVC%JWcXuEluu3H5E)SBax z%J7L*YvmVZ#(iN6i1a`$ds#=v3aSTt`~w)V8Aga5HRrF}^s-DAQK(A?+6$jIr7B%u zzE@YMj~T3oxZlK(p?VPT(7?8*lQj}#}(^ahx?fEObOd2vWx`SUiyP@+&-zq(?d0@5huK+BxdMt!I$ z51Mou zsGJSBzs0XjIB5$8On|hxd7)2dt5=D-31_G|`$wXz(BjZvlV6hl{3A!zPTHR!By8Rb zD?1r)s_0gpGYS=oZ>Nt$B--@R-&5QY1OFIae*P#q$H93wH@j z@A6n)MbJ|wRM&D6mmJs`36-Px=)HY7_d%2va5UI1mJ}xcSe;r-N4Lb3ji$TVcyZKm zePxo#*`98DTW{42y+*%?3pw!%bqruv^m&UCNPEs_XI-3_Zf9+shK?IJX^cY13qtOl z^0dydsb~GOw=v29ZScUi9A{5w6E(`aE@4Wnpz%$$H>k0&RK`;eaS9hset!=d$B4v~ z6fQnIOERD3DwXt$FLD`0yTy7|;>e$Wa?a_~y`l60QmBsXyYCI<^B2RK*T`P&M5Ao$zQ|J&vR`!B z+Iem*^BIMZ7}Qs6hnD>64{8K#tdb@52v$I7-Gw&*QObLQ!J6Rguzs792)4q401UsF z0^)^TZRS_1jqRUkUwrfsj?a_FU3p3GfZ;gyP)tu55}&O>Y%hc46*ss2wV;0e@T{4E zn-nZSARvn*C3u2zMO++Dgw;TLZOzezx4tY6&kbj|$bMBKl`^4@K&BHqCRb%WLtnxP z$HWaFEUBz)Oy)9LA(&VHlU#nn%3rTpDfqX_?9*C4KSC@ZR^-N%TAJT#NR>4*j+G%j z$PcXIy<@K~UcK%lxYW>awFn*$-zJy~d0m>9*Yk|-*vO)F;7j9e;fF6DF(V1y#;jpE z;(pO4p3yB1jpwq;&B(cMUL@3Vr09O&HPG7onL2Kf4{JB){-F(o=$kWbp3gMZVQ`10 zCt4(Yu*v^2!$D{g18Ba^CzX9?l_(wcRntoe`#o#L1P*I!?jbo*R~9a$Y8=C9$!wB5h_75J3pAsUt5R$|EfN zOaL!Gr;pzhCk=Ip+cO3DC>;`zpJ@AJ!vfD#=Viticv!?-r43AymHhn4T~t(r^StB} zKS^DF7)>-~5y=?NFfEB(-HXzseNl_q80`gAwr@!z)0#3eX&M?DWG>$sb-TfjfBXbL z=9Q4Bxj5TOh%lGWL?v@FpowIm*(gEFo8vRkT2Px0W3c06v=@VnaAv0sn~bHi8(r{| zgWtB7f#1GSP+(+gq^U_RjewEL>(9lI(wo{~f5$-OcvH0h3gyu3!}_x@;|z=6u}p;$ zG?DY58x}M=<6qMTKCG6Yn_2e1;rq_YO*6<}r$d7hQqsU*$d3mU4JidHKv?M%^=xP~ zH~|ILIWk$10Jpr4#u-I0Qc|-YF&ddFOzvyySsPd^9NI}q@tGP_%87~3@@rC2y7|8* zqF}54|IRgDjgtM@P??tDt`e(IQC?+KOXY0kerxQ0t2`$#Sj+A{c2VC}lQ&&bz8Ref zdFe;5mKt$(_9ef%dQw|dgjK-1hd3#ZDET%LCFe4`>6|rms43KWHeNA(`t+nQkvlU5Ct4BwSFe3=PxpRKLk;Ipv Date: Tue, 24 Nov 2020 23:31:19 +0100 Subject: [PATCH 14/17] Fixing a test --- src/Model/Behavior/FileAssociationBehavior.php | 3 ++- tests/TestCase/Model/Table/ItemsTableTest.php | 14 +++++++------- tests/bootstrap.php | 4 ---- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/Model/Behavior/FileAssociationBehavior.php b/src/Model/Behavior/FileAssociationBehavior.php index fb2b6fd..59debde 100644 --- a/src/Model/Behavior/FileAssociationBehavior.php +++ b/src/Model/Behavior/FileAssociationBehavior.php @@ -42,6 +42,7 @@ public function initialize(array $config): void $defaults = [ 'replace' => $associationObject instanceof HasOne, 'model' => $model, + 'collection' => $association, 'property' => $this->getTable()->getAssociation($association)->getProperty(), ]; @@ -136,8 +137,8 @@ protected function findAndRemovePreviousFile( ): void { $result = $this->getTable()->{$association}->find() ->where([ - 'collection' => $assocConfig['collection'], 'model' => $assocConfig['model'], + 'collection' => $assocConfig['collection'] ?? null, 'foreign_key' => $entity->get((string)$this->getTable()->getPrimaryKey()), 'id !=' => $entity->get($assocConfig['property'])->get((string)$this->getTable()->{$association}->getPrimaryKey()), ]) diff --git a/tests/TestCase/Model/Table/ItemsTableTest.php b/tests/TestCase/Model/Table/ItemsTableTest.php index 088df97..f137ac8 100644 --- a/tests/TestCase/Model/Table/ItemsTableTest.php +++ b/tests/TestCase/Model/Table/ItemsTableTest.php @@ -42,6 +42,7 @@ public function setUp(): void ], 'joinType' => 'LEFT', ]); + $this->table->hasMany('Photos', [ 'className' => 'Burzum/FileStorage.FileStorage', 'foreignKey' => 'foreign_key', @@ -75,8 +76,7 @@ public function tearDown(): void { parent::tearDown(); - unset($this->FileStorage); - unset($this->table); + unset($this->FileStorage, $this->table); $this->getTableLocator()->clear(); } @@ -108,7 +108,7 @@ public function testUploadNew() $this->assertSame('Items', $entity->avatar->model); $this->assertNotEmpty($entity->avatar->foreign_key); $this->assertSame('Avatars', $entity->avatar->collection); - $this->assertStringStartsWith('Avatars/', $entity->avatar->path); + $this->assertStringStartsWith('Avatars', $entity->avatar->path); $this->assertNotEmpty($entity->avatar->metadata); $this->assertNotEmpty($entity->avatar->variants); } @@ -136,7 +136,7 @@ public function testUploadOverwriteExisting() $this->table->saveOrFail($entity); $entity = $this->table->get($entity->id, ['contain' => 'Avatars']); -debug(json_encode($entity->avatar)); + $this->assertNotEmpty($entity->avatar); $expected = [ @@ -168,13 +168,13 @@ public function testUploadOverwriteExisting() $this->assertSame('Items', $entity->avatar->model); $this->assertNotEmpty($entity->avatar->foreign_key); $this->assertSame('Avatars', $entity->avatar->collection); - $this->assertStringStartsWith('Avatars/', $entity->avatar->path); + $this->assertStringStartsWith('Avatars', $entity->avatar->path); $this->assertNotEmpty($entity->avatar->metadata); $this->assertNotEmpty($entity->avatar->variants); -debug(json_encode($entity->avatar)); + $expected = [ 'width' => 512, - 'height' => 512, + 'height' => 768, ]; $this->assertSame($expected, $entity->avatar->metadata); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index dc92d2d..b658a92 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -2,10 +2,6 @@ declare(strict_types = 1); -/** - * Bootstrap - */ - use Cake\Core\Plugin; $findRoot = function ($root) { From ebf1199f569ef996bd9be75fd27e49e4b42a7ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Tue, 24 Nov 2020 23:44:59 +0100 Subject: [PATCH 15/17] Updating docs, mostly removing deprecated docs --- README.md | 35 +---- .../Getting-a-File-Path-and-URL.md | 102 ------------- docs/Documentation/How-To-Use.md | 134 +----------------- docs/Documentation/How-it-works.md | 2 +- .../List-of-included-Adapters.md | 24 ---- docs/README.md | 10 -- docs/Tutorials/Replacing-Files.md | 4 - docs/Tutorials/Validating-File-Uploads.md | 36 ----- 8 files changed, 7 insertions(+), 340 deletions(-) delete mode 100644 docs/Documentation/Getting-a-File-Path-and-URL.md delete mode 100644 docs/Documentation/List-of-included-Adapters.md delete mode 100644 docs/Tutorials/Replacing-Files.md delete mode 100644 docs/Tutorials/Validating-File-Uploads.md diff --git a/README.md b/README.md index 0b22f91..53c947d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ FileStorage Plugin for CakePHP **If you're upgrading from CakePHP 2.x please read [the migration guide](docs/Documentation/Migrating-from-CakePHP-2.md).** -The **File Storage** plugin is giving you the possibility to upload and store files in virtually any kind of storage backend. The plugin features the [Gaufrette](https://github.com/KnpLabs/Gaufrette) **and** [FlySystem](https://github.com/thephpleague/flysystem) library in a CakePHP fashion and provides a simple way to use the storage adapters through the [StorageManager](src/Storage/StorageManager.php) class. +The **File Storage** plugin is giving you the possibility to upload and store files in virtually any kind of storage backend. The plugin features the [FlySystem](https://github.com/thephpleague/flysystem) library in a CakePHP fashion and provides a simple way to use the storage adapters. Storage adapters are an unified interface that allow you to store file data to your local file system, in memory, in a database or into a zip file and remote systems. There is a database table keeping track of what you stored where. You can always write your own adapter or extend and overload existing ones. @@ -17,47 +17,22 @@ How it works The whole plugin is build with clear [Separation of Concerns (SoC)](https://en.wikipedia.org/wiki/Separation_of_concerns) in mind: A file is *always* an entry in the `file_storage` table from the app perspective. The table is the *reference* to the real place of where the file is stored and keeps some meta information like mime type, filename, file hash (optional) and size as well. Storing the path to a file inside an arbitrary table along other data is considered as *bad practice* because it doesn't respect SoC from an architecture perspective but many people do it this way for some reason. -You associate the `file_storage` table with your model using the FileStorage or ImageStorage model from the plugin via hasOne, hasMany or HABTM. When you upload a file you save it to the FileStorage model through the associations, `Documents.file` for example. The FileStorage model dispatches then file storage specific events, the listeners listening to these events process the file and put it in the configured storage backend using adapters for different backends and build the storage path using a path builder class. +You associate the `file_storage` table with your model using the FileStorage model from the plugin via hasOne, hasMany or HABTM. When you upload a file you save it to the FileStorage model through the associations, `Documents.file` for example. The FileStorage model dispatches then file storage specific events, the listeners listening to these events process the file and put it in the configured storage backend using adapters for different backends and build the storage path using a path builder class. -List of supported Adapters --------------------------- - - * Apc - * Amazon S3 - * ACL Aware Amazon S3 - * Azure - * Doctrine DBAL - * Dropbox - * Ftp - * Grid FS - * In Memory - * Local File System - * MogileFS - * Open Cloud - * Rackspace Cloudfiles - * Sftp - * Zip File Supported CakePHP Versions -------------------------- - * CakePHP 4.x -> 3.0 Branch + * CakePHP 4.x -> 4.0 Branch (Rewritten almost from scratch) + * CakePHP 4.x -> 3.0 Branch (Old codebase) * CakePHP 3.x -> 2.0 Branch * CakePHP 2.x -> 1.0 Branch Requirements ------------ - * PHP 7.2+ + * PHP 7.4+ * CakePHP 4.x - * Gaufrette Storage Library 0.7.x - -Optional but required if you want image processing out of the box: - - * The [Imagine Image processing plugin](https://github.com/burzum/cakephp-imagine-plugin) if you want to process and store images. - * [FlySystem](https://github.com/thephpleague/flysystem) as alternative library over Gaufrette - -You can still implement whatever file processing you want very easy. It's not tied to Imagine. Documentation ------------- diff --git a/docs/Documentation/Getting-a-File-Path-and-URL.md b/docs/Documentation/Getting-a-File-Path-and-URL.md deleted file mode 100644 index 0e59b58..0000000 --- a/docs/Documentation/Getting-a-File-Path-and-URL.md +++ /dev/null @@ -1,102 +0,0 @@ -# Getting a file path and URL - -The path and filename of a stored file in the storage backend that was used to store the file is generated by a [path builder](Path-Builders.md). The event listener that stored your file has used a path builder to generate the path based on the entity data. This means that if you have the entity and instantiate a path builder you can build the path to it in any place. - -The plugin already provides you with several convenience short cuts to do that. - -Be aware that whenever you use a path builder somewhere, you **must** use the same path builder and options as when the entity was created. They're usually the same as configured in your event listener. - -## Getting it from an entity - -If you're using an entity from this plugin, or extending it they'll implement the PathBuilderTrait. This enables you to set and get the path builder on the entities. - -Due to some [limitations of the CakePHP core](http://api.cakephp.org/3.1/source-class-Cake.ORM.Table.html#1965) you can't pass options to the entity when calling `Table::newEntity()`. - -There are two workarounds for that issue. Either you'll have to set it manually on the entity instance: - -```php -$entity->pathBuilder('PathBuilderName', ['options-array' => 'goes-here']); -$entity->path(); // Gets you the path in the used storage backend to the file -$entity->url(); // Gets you the URL to the file if possible -``` - -Or do it in the constructor of the entity. Pay attention to the two properties `_pathBuilderClass` and `_pathBuilderOptions`. Set whatever you need here. If you're inheriting `Burzum\FileStorage\Model\Entity\FileStorage` these options and the code below will be already present. - -```php -namespace App\Model\Entity; - -use Burzum\FileStorage\Storage\PathBuilder\PathBuilderTrait; - -class SomeEntityInYourApp extends Entity { - - use PathBuilderTrait; - - /** - * Path Builder Class. - * - * This is named $_pathBuilderClass because $_pathBuilder is already used by - * the trait to store the path builder instance. - * - * @param array - */ - protected $_pathBuilderClass = null; - - /** - * Path Builder options - * - * @param array - */ - protected $_pathBuilderOptions = []; - - /** - * Constructor - * - * @param array $properties hash of properties to set in this entity - * @param array $options list of options to use when creating this entity - */ - public function __construct(array $properties = [], array $options = []) { - $options += [ - 'pathBuilder' => $this->_pathBuilderClass, - 'pathBuilderOptions' => $this->_pathBuilderOptions - ]; - - parent::__construct($properties, $options); - - if (!empty($options['pathBuilder'])) { - $this->pathBuilder( - $options['pathBuilder'], - $options['pathBuilderOptions'] - ); - } - } -} -``` - -If you want to use path builders depending on the kind of file or the identifier which is stored in the `model` field of the `file_storage` table, you can implement that logic as well there and use the entities data to determine the path builder class or options. - -## Getting it using the storage helper - -The storage helper is basically just a proxy to a path builder. The helper takes two configuration options: - - * **pathBuilder**: Name of the path builder to use. - * **pathBuilderOptions**: The options passed to the path builders constructor. - -Make sure that the options you pass and the path builder are the same you've used when you uploaded the file! Otherwise you end up with a different path! - -```php -// Load the helper -$this->loadHelper('Burzum/FileStorage.Storage', [ - 'pathBuilder' => 'Base', - // The builder options must match the options and builder class that were used to store the file! - 'pathBuilderOptions' => [ - 'modelFolder' => true, - ] -]); - -// Use it in your views -$url = $this->Storage->url($yourEntity); - -// Change the path builder at run time -// Be carefully, this will change the path builder instance in the helper! -$this->Storage->pathBuilder('SomePathBuilder', ['options' => 'here']); -``` diff --git a/docs/Documentation/How-To-Use.md b/docs/Documentation/How-To-Use.md index 721b073..7b26764 100644 --- a/docs/Documentation/How-To-Use.md +++ b/docs/Documentation/How-To-Use.md @@ -1,136 +1,4 @@ How to Use It ============= -Before you continue to read this page it is recommended that you have read about [the Storage Manager](The-Storage-Manager.md) before. - -The following text is going to describe two ways to store a file. Which of both you choose depends at the end on your use case but it is recommended to use the events because they automate the whole process much more. - -The basic idea of this plugin is that files are always handled as separate entities and are associated to other models. The reason for that is simple. A file has multiple properties like size, mime type and other entities in the system can have more than one file for example. It is considered as *bad* practice to store lots of file paths as reference in a table together with other data. - -This plugin resolves that issue by handling each file as a completely separate entity in the application. There is just one table `file_storage` that will keep the reference to all your files, no matter where they're stored. - -Preparing the File Upload -------------------------- - -This section is going to show how to store a file using the Storage Manager directly. - -For example you have a `reports` table and want to save a PDF to it, you would then create an association like: - -```php -public function initialize(array $config) -{ - parent::initialize($config); - $this->table('reports'); - - $this->hasOne('PdfFiles', [ - 'className' => 'Burzum/FileStorage.PdfFiles', - 'foreignKey' => 'foreign_key', - 'conditions' => [ - 'PdfFiles.model' => 'Reports' - ] - ]); -} -``` - -In your `add.ctp` or `edit.ctp` views you would add something like this. - -```php -echo $this->Form->create($report, ['type' => 'file']); -echo $this->Form->input('title'); -echo $this->Form->file('pdf_files.file'); // Pay attention here! -echo $this->Form->input('description'); -echo $this->Form->submit(__('Submit')); -echo $this->Form->end(); -``` - -[Make sure your form is using the right HTTP method](http://book.cakephp.org/3.0/en/views/helpers/form.html#changing-the-http-method-for-a-form)! - -Store an uploaded file using Events ------------------------------------ - -The **FileStorage** plugin comes with a class that acts just as a listener to some of the events in this plugin. Take a look at [ImageProcessingListener.php](../../src/Event/ImageProcessingListener.php). - -This class will listen to all the ImageStorage model events and save the uploaded image and then create the versions for that image and storage adapter. - -It is important to understand that nearly each storage adapter requires a little different handling: Most of the time you can't treat a local file the same as a file you store in a cloud service. -The interface that this plugin and Gaufrette provide is the same but not the internals. So a path that works for your local file system might not work for your remote storage system because it has other requirements or limitations. - -So if you want to store a file using Amazon S3 you would have to store it, -create all the versions of that image locally and then upload each of them -and then delete the local temp files. The good news is the plugin can already take care of that. - -When you create a new listener it is important that you check the `model` field and -the event subject object (usually a table object inheriting `\Cake\ORM\Table`) if it -matches what you expect. -Using the event system you could create any kind of storage and upload behavior without -inheriting or touching the model code. Just write a listener class and attach it to the global EventManager. - -List of events --------------- - -Events triggered in the `ImageStorage` model: - - * ImageVersion.createVersion - * ImageVersion.removeVersion - * ImageStorage.beforeSave - * ImageStorage.afterSave - * ImageStorage.beforeDelete - * ImageStorage.afterDelete - -Events triggered in the `FileStorage` model: - - * FileStorage.beforeSave - * FileStorage.afterSave - * FileStorage.afterDelete - -Event Listeners ---------------- - -See [this page](Included-Event-Listeners.md) for the event listeners that are included in the plugin. - - -Handling the File Upload Manually ---------------------------------- - -You'll have to customize it a little but its just a matter for a few lines. - -Note the Listener expects a request data key `file` present in the form, so use `echo $this->Form->input('file');` to allow the Marshaller pick the right data from the uploaded file. - -Lets go by this scenario inside the report table, assuming there is an add() method: - -```php -public function add() { - $entity = $this->newEntity($postData); - $saved = $this->save($entity); - if ($saved) { - $key = 'your-file-name'; - if (StorageManager::get('Local')->write($key, file_get_contents($this->data['pdf_files']['file']['tmp_name']))) { - $postData['pdf_files']['foreign_key'] = $saved->id; - $postData['pdf_files']['model'] = 'Reports'; - $postData['pdf_files']['path'] = $key; - $postData['pdf_files']['adapter'] = 'Local'; - $this->PdfDocuments->save($this->PdfDocuments->newEntity($postData)); - } - } - return $entity; -} -``` - -Later, when you want to delete the file, for example in the beforeDelete() or afterDelete() callback of your Report model, you'll know the adapter you have used to store the attached PdfFile and can get an instance of this adapter configuration using the StorageManager. By having the path or key available you can then simply call: - -```php -StorageManager::get($data['PdfFile']['adapter'])->delete($data['PdfFile']['path']); -``` - -Insted of doing all of this in the table object that has the files associated to it you can also simply extend the FileStorage table from the plugin and add your storage logic there and use that table for your association. - -Why is it done like this? -------------------------- - -Every developer might want to store the file at a different point or apply other operations on the file before or after it is stored. Based on different circumstances you might want to save an associated file even before you created the record its going to get attached to, in other scenarios like in this documentation you might want to do it after. - -The ``$key`` is also a key aspect of it: Different adapters might expect a different key. A key for the Local adapter of Gaufrette is usually a path and a file name under which the data gets stored. That's also the reason why you use `file_get_contents()` instead of simply passing the tmp path as it is. - -It is up to you how you want to generate the key and build your path. You can customize the way paths and file names are build by writing a custom event listener for that. - -It is highly recommended to read the Gaufrette documentation for the read() and write() methods of the adapters. +TO BE DONE diff --git a/docs/Documentation/How-it-works.md b/docs/Documentation/How-it-works.md index 93d0a6c..8b7d9d9 100644 --- a/docs/Documentation/How-it-works.md +++ b/docs/Documentation/How-it-works.md @@ -3,7 +3,7 @@ How it works The whole plugin is build with clear [Separation of Concerns](https://en.wikipedia.org/wiki/Separation_of_concerns) in mind: A file is *always* an entry in the `file_storage` table from the app perspective. -The table is the *reference* to the real place of where the file is stored and keeps some meta information like mime type, filename, file hash (optional) and size as well. You associate the `file_storage` table with your model using the FileStorage or ImageStorage model from the plugin via hasOne, hasMany or HABTM. +The table is the *reference* to the real place of where the file is stored and keeps some meta information like mime type, filename, file hash (optional) and size as well. You associate the `file_storage` table with your model using the FileStorage model from the plugin via hasOne, hasMany or HABTM. When you upload a file you save it to the FileStorage model through the associations, `Documents.file` for example. The FileStorage model dispatches then file storage specific events. [The listeners](../../src/Storage/Listener) listening to these events process the file and put it in the configured storage backend using adapters for different backends and build the storage path using a [path builder](Path-Builders.md) class. diff --git a/docs/Documentation/List-of-included-Adapters.md b/docs/Documentation/List-of-included-Adapters.md deleted file mode 100644 index 6c4feef..0000000 --- a/docs/Documentation/List-of-included-Adapters.md +++ /dev/null @@ -1,24 +0,0 @@ -Included storage adapters -------------------------- - -The following adapters are coming along with the Gaufrette library that is used by this plugin as a base. - - * Apc - * Amazon S3 - * ACL Aware Amazon S3 - * Azure - * Doctrine DBAL - * Dropbox - * Ftp - * Grid FS - * In Memory - * Local File System - * MogileFS - * Open Cloud - * Rackspace Cloudfiles - * Sftp - * Zip File - -Check [the adapter folder](https://github.com/KnpLabs/Gaufrette/tree/master/src/Gaufrette/Adapter) of the Gaufrette lib for a complete list. - -If you need another adapter that is not included you can implement it yourself or try searching the web first. diff --git a/docs/README.md b/docs/README.md index 466c4c4..4c438a1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,22 +1,12 @@ # Home The **File Storage** plugin is giving you the possibility to store files in virtually any kind of storage backend. -This plugin is wrapping the [Gaufrette](https://github.com/KnpLabs/Gaufrette) library in a CakePHP fashion -and provides a simple way to use the storage adapters through the [StorageManager](../Lib/StorageManager.php) class. - -[See this list of included storage adapters.](Docs/Documentation/List-of-included-Adapters.md) - -Storage adapters are an unified interface that allow you to store file data to your local file system, in memory, in a database or into a zip file and remote systems. There is a database table keeping track of what you stored were. You can always write your own adapter or extend and overload existing ones. ## Documentation * [Installation](Documentation/Installation.md) * [How it works](Documentation/How-it-works.md) * [How to Use it](Documentation/How-To-Use.md) -* [Migrating from File Storage v1 to v2](Migrating-from-File-Storage-v1-to-v2.md) -* [The Storage Manager](Documentation/The-Storage-Manager.md) -* [Included Event Listeners](Documentation/Included-Event-Listeners.md) -* [Path Builders](Documentation/Path-Builders.md) * [Getting a file path and URL](Documentation/Getting-a-File-Path-and-URL.md) * Image processing * [Image Storage and Versioning](Documentation/Image-Storage-And-Versioning.md) diff --git a/docs/Tutorials/Replacing-Files.md b/docs/Tutorials/Replacing-Files.md deleted file mode 100644 index 666b497..0000000 --- a/docs/Tutorials/Replacing-Files.md +++ /dev/null @@ -1,4 +0,0 @@ -Replacing Files -=============== - -**Don't** use Table::deleteAll() if you don't want to end up with orphaned files! The reason for that is that deleteAll() doesn't fire the callbacks. So the events that will remove the files won't get fired. diff --git a/docs/Tutorials/Validating-File-Uploads.md b/docs/Tutorials/Validating-File-Uploads.md deleted file mode 100644 index f0c86a8..0000000 --- a/docs/Tutorials/Validating-File-Uploads.md +++ /dev/null @@ -1,36 +0,0 @@ -Validating File Uploads -======================= - -You can validate your uploads by extending `Burzum\FileStorage\Storage\Listener\ValidationListener` and implement your validation methods just like you do in table objects: - -```php -use Burzum\FileStorage\Storage\Listener\ValidationListener; -use Cake\Validation\Validator; - -class TestValidationListener extends ValidationListener { - - public function validationAvatar(Validator $validator) { - $validator->add('file', 'mimeType', [ - 'rule' => ['mimeType', ['image/jpg', 'image/jpeg', 'image/png']] - ]); - - $validator->add('file', 'imageSize', [ - 'rule' => ['imageSize', [ - 'height' => ['>=', 200], - 'width' => ['>=', 200] - ]] - ]); - - return $validator; - } -} - -EventManager::instance()->on(new FileValidationListener()); -``` - -This will attach the listener to the `Model.initialize()` event and add your configured validators to the FileStorage table. - -References: - -* (Using A Different Validation Set)[http://book.cakephp.org/3.0/en/orm/validation.html#using-a-different-validation-set] -* (Table::validator())[http://api.cakephp.org/3.3/class-Cake.Validation.ValidatorAwareTrait.html#_validator] From 2ccb0a575505db08f1110f976ae6326154ca0b3a Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 25 Nov 2020 00:56:13 +0100 Subject: [PATCH 16/17] Revert wrong test assumption --- tests/TestCase/Model/Table/ItemsTableTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestCase/Model/Table/ItemsTableTest.php b/tests/TestCase/Model/Table/ItemsTableTest.php index f137ac8..08223b7 100644 --- a/tests/TestCase/Model/Table/ItemsTableTest.php +++ b/tests/TestCase/Model/Table/ItemsTableTest.php @@ -174,7 +174,7 @@ public function testUploadOverwriteExisting() $expected = [ 'width' => 512, - 'height' => 768, + 'height' => 512, // !!! ]; $this->assertSame($expected, $entity->avatar->metadata); } From 890e8a8e15ddf8ac35f7b812c9650e9e4f263541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Kr=C3=A4mer?= Date: Wed, 25 Nov 2020 01:07:21 +0100 Subject: [PATCH 17/17] Adding a bunch of storage related events. --- src/Model/Behavior/FileStorageBehavior.php | 23 +++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Model/Behavior/FileStorageBehavior.php b/src/Model/Behavior/FileStorageBehavior.php index 271df93..9b6053e 100644 --- a/src/Model/Behavior/FileStorageBehavior.php +++ b/src/Model/Behavior/FileStorageBehavior.php @@ -180,14 +180,35 @@ public function afterSave(EventInterface $event, EntityInterface $entity, ArrayO if ($entity->isNew()) { try { $file = $this->entityToFileObject($entity); + + $this->dispatchEvent('FileStorage.beforeStoringFile', [ + 'entity' => $entity, + 'file' => $file, + ], $this->getTable()); + $file = $this->fileStorage->store($file); - // TODO: move into stack processing + $this->dispatchEvent('FileStorage.afterStoringFile', [ + 'entity' => $entity, + 'file' => $file, + ], $this->getTable()); + $file = $this->processImages($file, $entity); $processor = $this->getFileProcessor(); + + $this->dispatchEvent('FileStorage.beforeFileProcessing', [ + 'entity' => $entity, + 'file' => $file, + ], $this->getTable()); + $file = $processor->process($file); + $this->dispatchEvent('FileStorage.afterFileProcessing', [ + 'entity' => $entity, + 'file' => $file + ], $this->getTable()); + $entity = $this->fileObjectToEntity($file, $entity); $this->getTable()->saveOrFail( $entity,