From dd9e53a7938f5cb4cef3b6e8ce95bceb2dc460a7 Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Wed, 2 Feb 2022 21:33:58 +0100 Subject: [PATCH 1/5] Code cleanup & logging fixes --- Classes/AssetSource/PixxioAssetProxy.php | 16 ++++---- Classes/AssetSource/PixxioAssetProxyQuery.php | 38 ++++++++++++++----- .../PixxioAssetProxyQueryResult.php | 8 ++-- .../PixxioAssetProxyRepository.php | 17 ++++----- Classes/Command/PixxioCommandController.php | 1 - Classes/Controller/PixxioController.php | 8 +--- README.md | 26 ++++++------- 7 files changed, 64 insertions(+), 50 deletions(-) diff --git a/Classes/AssetSource/PixxioAssetProxy.php b/Classes/AssetSource/PixxioAssetProxy.php index e834722..66c19f3 100644 --- a/Classes/AssetSource/PixxioAssetProxy.php +++ b/Classes/AssetSource/PixxioAssetProxy.php @@ -14,10 +14,12 @@ */ use Behat\Transliterator\Transliterator; -use GuzzleHttp\Psr7\Uri; use Exception; use Flownative\Pixxio\Exception\ConnectionException; use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\Psr7\Uri; +use Neos\Flow\Annotations as Flow; use Neos\Media\Domain\Model\AssetSource\AssetProxy\AssetProxyInterface; use Neos\Media\Domain\Model\AssetSource\AssetProxy\HasRemoteOriginalInterface; use Neos\Media\Domain\Model\AssetSource\AssetProxy\SupportsIptcMetadataInterface; @@ -26,7 +28,6 @@ use Neos\Media\Domain\Repository\ImportedAssetRepository; use Neos\Utility\MediaTypes; use Psr\Http\Message\UriInterface; -use Neos\Flow\Annotations as Flow; use stdClass; /** @@ -152,7 +153,7 @@ public static function fromJsonObject(stdClass $jsonObject, PixxioAssetSource $a if (isset($modifiedImagePaths[2])) { $assetProxy->originalUri = new Uri($modifiedImagePaths[2]); } - } else if (is_object($modifiedImagePaths)) { + } elseif (is_object($modifiedImagePaths)) { if (isset($modifiedImagePaths->{'0'})) { $assetProxy->thumbnailUri = new Uri($modifiedImagePaths->{'0'}); } @@ -285,7 +286,8 @@ public function getPreviewUri(): ?UriInterface } /** - * @return resource + * @return bool|resource + * @throws ConnectionException */ public function getImportStream() { @@ -294,11 +296,11 @@ public function getImportStream() $response = $client->request('GET', $this->originalUri); if ($response->getStatusCode() === 200) { return $response->getBody()->detach(); - } else { - return false; } + + return false; } catch (GuzzleException $e) { - throw new ConnectionException('Retrieving file failed: ' . $e->getMessage(), 1542808207); + throw new ConnectionException('Retrieving file failed: ' . $e->getMessage(), 1542808207, $e); } } diff --git a/Classes/AssetSource/PixxioAssetProxyQuery.php b/Classes/AssetSource/PixxioAssetProxyQuery.php index db82cfa..6c60a3d 100644 --- a/Classes/AssetSource/PixxioAssetProxyQuery.php +++ b/Classes/AssetSource/PixxioAssetProxyQuery.php @@ -18,9 +18,11 @@ use Flownative\Pixxio\Exception\MissingClientSecretException; use GuzzleHttp\Psr7\Response; use Neos\Flow\Annotations\Inject; -use Psr\Log\LoggerInterface as SystemLoggerInterface; +use Neos\Flow\Log\ThrowableStorageInterface; +use Neos\Flow\Log\Utility\LogEnvironment; use Neos\Media\Domain\Model\AssetSource\AssetProxyQueryInterface; use Neos\Media\Domain\Model\AssetSource\AssetProxyQueryResultInterface; +use Psr\Log\LoggerInterface; /** * @@ -64,10 +66,16 @@ final class PixxioAssetProxyQuery implements AssetProxyQueryInterface /** * @Inject - * @var SystemLoggerInterface + * @var LoggerInterface */ protected $logger; + /** + * @Inject + * @var ThrowableStorageInterface + */ + protected $throwableStorage; + /** * @param PixxioAssetSource $assetSource */ @@ -92,7 +100,6 @@ public function getOffset(): int return $this->offset; } - /** * @param int $limit */ @@ -112,7 +119,7 @@ public function getLimit(): int /** * @param string $searchTerm */ - public function setSearchTerm(string $searchTerm) + public function setSearchTerm(string $searchTerm): void { $this->searchTerm = $searchTerm; } @@ -128,7 +135,7 @@ public function getSearchTerm(): string /** * @param string $assetTypeFilter */ - public function setAssetTypeFilter(string $assetTypeFilter) + public function setAssetTypeFilter(string $assetTypeFilter): void { $this->assetTypeFilter = $assetTypeFilter; } @@ -192,22 +199,30 @@ public function count(): int if (!isset($responseObject->quantity)) { if (isset($responseObject->help)) { - $this->logger->logException(new ConnectionException('Connection to pixx.io failed: ' . $responseObject->help, 1526629493)); + $message = $this->throwableStorage->logThrowable(new ConnectionException('Query to pixx.io failed: ' . $responseObject->help, 1526629493)); + $this->logger->error($message, LogEnvironment::fromMethodName(__METHOD__)); } return 0; } return $responseObject->quantity; } catch (AuthenticationFailedException $exception) { - $this->logger->logException(new ConnectionException('Connection to pixx.io failed: ' . $exception->getMessage(), 1526629541)); + $message = $this->throwableStorage->logThrowable(new ConnectionException('Connection to pixx.io failed.', 1526629541, $exception)); + $this->logger->error($message, LogEnvironment::fromMethodName(__METHOD__)); return 0; } catch (MissingClientSecretException $exception) { - $this->logger->logException(new ConnectionException('Connection to pixx.io failed: ' . $exception->getMessage(), 1526629547)); + $message = $this->throwableStorage->logThrowable(new ConnectionException('Connection to pixx.io failed.', 1526629547, $exception)); + $this->logger->error($message, LogEnvironment::fromMethodName(__METHOD__)); + return 0; + } catch (ConnectionException $exception) { + $message = $this->throwableStorage->logThrowable(new ConnectionException('Connection to pixx.io failed.', 1643823324, $exception)); + $this->logger->error($message, LogEnvironment::fromMethodName(__METHOD__)); return 0; } } /** * @return PixxioAssetProxy[] + * @throws \Exception */ public function getArrayResult(): array { @@ -220,10 +235,12 @@ public function getArrayResult(): array $assetProxies[] = PixxioAssetProxy::fromJsonObject($rawAsset, $this->assetSource); } } catch (AuthenticationFailedException $exception) { - $this->logger->logException(new ConnectionException('Connection to pixx.io failed: ' . $exception->getMessage(), 1526629541)); + $message = $this->throwableStorage->logThrowable(new ConnectionException('Connection to pixx.io failed.', 1643822709, $exception)); + $this->logger->error($message, LogEnvironment::fromMethodName(__METHOD__)); return []; } catch (MissingClientSecretException $exception) { - $this->logger->logException(new ConnectionException('Connection to pixx.io failed: ' . $exception->getMessage(), 1526629547)); + $message = $this->throwableStorage->logThrowable(new ConnectionException('Connection to pixx.io failed.', 1643822727, $exception)); + $this->logger->error($message, LogEnvironment::fromMethodName(__METHOD__)); return []; } return $assetProxies; @@ -235,6 +252,7 @@ public function getArrayResult(): array * @return Response * @throws AuthenticationFailedException * @throws MissingClientSecretException + * @throws ConnectionException */ private function sendSearchRequest(int $limit, array $orderings): Response { diff --git a/Classes/AssetSource/PixxioAssetProxyQueryResult.php b/Classes/AssetSource/PixxioAssetProxyQueryResult.php index 778fee4..946d876 100644 --- a/Classes/AssetSource/PixxioAssetProxyQueryResult.php +++ b/Classes/AssetSource/PixxioAssetProxyQueryResult.php @@ -103,7 +103,6 @@ public function key() { $this->initialize(); return $this->assetProxiesIterator->key(); - } public function valid() @@ -142,15 +141,18 @@ public function offsetUnset($offset) /** * @return int + * @throws \Flownative\Pixxio\Exception\ConnectionException */ public function count(): int { if ($this->numberOfAssetProxies === null) { if (is_array($this->assetProxies)) { - return count($this->assetProxies); + $this->numberOfAssetProxies = count($this->assetProxies); } else { - return $this->query->count(); + $this->numberOfAssetProxies = $this->query->count(); } } + + return $this->numberOfAssetProxies; } } diff --git a/Classes/AssetSource/PixxioAssetProxyRepository.php b/Classes/AssetSource/PixxioAssetProxyRepository.php index a6e44bc..ec5d9ab 100644 --- a/Classes/AssetSource/PixxioAssetProxyRepository.php +++ b/Classes/AssetSource/PixxioAssetProxyRepository.php @@ -20,7 +20,6 @@ use Flownative\Pixxio\Exception\ConnectionException; use Flownative\Pixxio\Exception\MissingClientSecretException; use Neos\Cache\Frontend\StringFrontend; -use Neos\Cache\Frontend\VariableFrontend; use Neos\Flow\ObjectManagement\DependencyInjection\DependencyProxy; use Neos\Media\Domain\Model\AssetSource\AssetNotFoundExceptionInterface; use Neos\Media\Domain\Model\AssetSource\AssetProxy\AssetProxyInterface; @@ -41,14 +40,6 @@ class PixxioAssetProxyRepository implements AssetProxyRepositoryInterface, Suppo */ private $assetSource; - /** - * @param PixxioAssetSource $assetSource - */ - public function __construct(PixxioAssetSource $assetSource) - { - $this->assetSource = $assetSource; - } - /** * @var string */ @@ -64,6 +55,14 @@ public function __construct(PixxioAssetSource $assetSource) */ protected $assetProxyCache; + /** + * @param PixxioAssetSource $assetSource + */ + public function __construct(PixxioAssetSource $assetSource) + { + $this->assetSource = $assetSource; + } + /** * @param string $identifier * @return AssetProxyInterface diff --git a/Classes/Command/PixxioCommandController.php b/Classes/Command/PixxioCommandController.php index f9c8c00..c6f54af 100644 --- a/Classes/Command/PixxioCommandController.php +++ b/Classes/Command/PixxioCommandController.php @@ -10,7 +10,6 @@ use Neos\Flow\Annotations as Flow; use Neos\Flow\Cli\CommandController; use Neos\Media\Domain\Model\Asset; -use Neos\Media\Domain\Model\AssetSource\AssetProxy\SupportsIptcMetadataInterface; use Neos\Media\Domain\Model\AssetSource\AssetSourceAwareInterface; use Neos\Media\Domain\Repository\AssetRepository; diff --git a/Classes/Controller/PixxioController.php b/Classes/Controller/PixxioController.php index cb330b2..e2714a9 100644 --- a/Classes/Controller/PixxioController.php +++ b/Classes/Controller/PixxioController.php @@ -12,12 +12,12 @@ * information, please view the LICENSE file which was distributed with this * source code. */ + use Flownative\Pixxio\AssetSource\PixxioAssetSource; use Flownative\Pixxio\Domain\Model\ClientSecret; use Flownative\Pixxio\Domain\Repository\ClientSecretRepository; use Flownative\Pixxio\Exception\AuthenticationFailedException; use Flownative\Pixxio\Exception\MissingClientSecretException; -use Flownative\Pixxio\Service\PixxioServiceFactory; use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\Exception\StopActionException; use Neos\Flow\Mvc\Exception\UnsupportedRequestTypeException; @@ -45,12 +45,6 @@ class PixxioController extends AbstractModuleController */ protected $clientSecretRepository; - /** - * @Flow\Inject - * @var PixxioServiceFactory - */ - protected $pixxioServiceFactory; - /** * @return void */ diff --git a/README.md b/README.md index 92811c0..caffeae 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # pixx.io adaptor for Neos -This [Flow](https://flow.neos.io) package allows you to use assets (ie. pictures and other documents) stored in [pixx.io](https://www.pixxio-bildverwaltung.de/) +This [Flow](https://flow.neos.io) package allows you to use assets (ie. pictures and other documents) stored in [pixx.io](https://www.pixx.io/) in your Neos website as if these assets were native Neos assets. ## About pixx.io @@ -74,7 +74,7 @@ Neos: When you committed and deployed these changes, you can log in to the Neos backend and navigate to the pixx.io backend module to verify your settings. - + If you would like a separate pixx.io user for the logged in Neos user, you can copy and paste your own "refresh token" into the form found in the backend module and store it along with your Neos user. @@ -106,7 +106,7 @@ Neos: ``` Sometimes the API Client needs additional configuration for the tls connection -like custom timeouts or certificates. +like custom timeouts or certificates. See: http://docs.guzzlephp.org/en/6.5/request-options.html ```yaml @@ -116,19 +116,19 @@ Neos: 'flownative-pixxio': assetSource: 'Flownative\Pixxio\AssetSource\PixxioAssetSource' assetSourceOptions: - apiClientOptions: - 'verify': '/path/to/cert.pem' + apiClientOptions: + 'verify': '/path/to/cert.pem' ``` ## Run database migrations ```bash ./flow doctrine:migrate -``` +``` ## Cleaning up unused assets Whenever a pixx.io asset is used in Neos, the media file will be copied automatically to the internal Neos asset -storage. As long as this media is used somewhere on the website, Neos will flag this asset as being in use. +storage. As long as this media is used somewhere on the website, Neos will flag this asset as being in use. When an asset is not used anymore, the binary data and the corresponding metadata can be removed from the internal storage. While this does not happen automatically, it can be easily automated by a recurring task, such as a cron-job. @@ -136,14 +136,14 @@ In order to clean up unused assets, simply run the following command as often as ```bash ./flow media:removeunused --asset-source flownative-pixxio -``` +``` If you'd rather like to invoke this command through a cron-job, you can add two additional flags which make this command non-interactive: ```bash ./flow media:removeunused --quiet --assume-yes --asset-source flownative-pixxio -``` +``` ## Auto-Tagging @@ -167,8 +167,8 @@ Neos: ``` Since Neos currently cannot handle auto-tagging reliably during runtime, the job must be done through a -command line command. Simply run the following command for tagging new assets and removing tags from -assets which are not in use anymore: +command line command. Simply run the following command for tagging new assets and removing tags from +assets which are not in use anymore: ``` ./flow pixxio:tagusedassets @@ -194,7 +194,7 @@ iterates over all assets which were imported from Pixxio and checks if tags need ## Credits and license -This plugin was sponsored by [pixx.io](https://www.pixxio-bildverwaltung.de/) and its initial version was developed by -Robert Lemke of [Flownative](https://www.flownative.com). +The first version of this plugin was sponsored by [pixx.io](https://www.pixxio-bildverwaltung.de/) and its initial +version was developed by Robert Lemke of [Flownative](https://www.flownative.com). See LICENSE for license details. From 43f66ae2cfe5e7ead0c1479c82354c90b1496667 Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Wed, 2 Feb 2022 21:47:08 +0100 Subject: [PATCH 2/5] Allow to filter by asset collection This allows to filter the assets through the use of asset collections. If the name of an asset collection matches a category in pixx.io, only the assets contained in that category (and it's sub-categories) are shown. --- Classes/AssetSource/PixxioAssetProxyQuery.php | 23 +++- .../PixxioAssetProxyRepository.php | 18 ++- Classes/Command/PixxioCommandController.php | 106 ++++++++++++++++++ Classes/Service/PixxioClient.php | 26 ++++- 4 files changed, 170 insertions(+), 3 deletions(-) diff --git a/Classes/AssetSource/PixxioAssetProxyQuery.php b/Classes/AssetSource/PixxioAssetProxyQuery.php index 6c60a3d..7e842ad 100644 --- a/Classes/AssetSource/PixxioAssetProxyQuery.php +++ b/Classes/AssetSource/PixxioAssetProxyQuery.php @@ -44,6 +44,11 @@ final class PixxioAssetProxyQuery implements AssetProxyQueryInterface */ private $assetTypeFilter = 'All'; + /** + * @var ?string + */ + private $assetCollectionFilter; + /** * @var array */ @@ -148,6 +153,22 @@ public function getAssetTypeFilter(): string return $this->assetTypeFilter; } + /** + * @param ?string $assetCollectionFilter + */ + public function setAssetCollectionFilter(?string $assetCollectionFilter): void + { + $this->assetCollectionFilter = $assetCollectionFilter; + } + + /** + * @return ?string + */ + public function getAssetCollectionFilter(): ?string + { + return $this->assetCollectionFilter; + } + /** * @return array */ @@ -282,6 +303,6 @@ private function sendSearchRequest(int $limit, array $orderings): Response break; } - return $this->assetSource->getPixxioClient()->search($searchTerm, $formatTypes, $fileTypes, $this->offset, $limit, $orderings); + return $this->assetSource->getPixxioClient()->search($searchTerm, $formatTypes, $fileTypes, $this->assetCollectionFilter, $this->offset, $limit, $orderings); } } diff --git a/Classes/AssetSource/PixxioAssetProxyRepository.php b/Classes/AssetSource/PixxioAssetProxyRepository.php index ec5d9ab..ab2bb1c 100644 --- a/Classes/AssetSource/PixxioAssetProxyRepository.php +++ b/Classes/AssetSource/PixxioAssetProxyRepository.php @@ -21,25 +21,32 @@ use Flownative\Pixxio\Exception\MissingClientSecretException; use Neos\Cache\Frontend\StringFrontend; use Neos\Flow\ObjectManagement\DependencyInjection\DependencyProxy; +use Neos\Media\Domain\Model\AssetCollection; use Neos\Media\Domain\Model\AssetSource\AssetNotFoundExceptionInterface; use Neos\Media\Domain\Model\AssetSource\AssetProxy\AssetProxyInterface; use Neos\Media\Domain\Model\AssetSource\AssetProxyQueryResultInterface; use Neos\Media\Domain\Model\AssetSource\AssetProxyRepositoryInterface; use Neos\Media\Domain\Model\AssetSource\AssetSourceConnectionExceptionInterface; use Neos\Media\Domain\Model\AssetSource\AssetTypeFilter; +use Neos\Media\Domain\Model\AssetSource\SupportsCollectionsInterface; use Neos\Media\Domain\Model\AssetSource\SupportsSortingInterface; use Neos\Media\Domain\Model\Tag; /** * PixxioAssetProxyRepository */ -class PixxioAssetProxyRepository implements AssetProxyRepositoryInterface, SupportsSortingInterface +class PixxioAssetProxyRepository implements AssetProxyRepositoryInterface, SupportsSortingInterface, SupportsCollectionsInterface { /** * @var PixxioAssetSource */ private $assetSource; + /** + * @var string|null + */ + protected $assetCollectionFilter; + /** * @var string */ @@ -114,6 +121,11 @@ public function filterByType(AssetTypeFilter $assetType = null): void $this->assetTypeFilter = (string)$assetType ?: 'All'; } + public function filterByCollection(AssetCollection $assetCollection = null): void + { + $this->assetCollectionFilter = $assetCollection ? $assetCollection->getTitle() : null; + } + /** * @return AssetProxyQueryResultInterface */ @@ -121,6 +133,7 @@ public function findAll(): AssetProxyQueryResultInterface { $query = new PixxioAssetProxyQuery($this->assetSource); $query->setAssetTypeFilter($this->assetTypeFilter); + $query->setAssetCollectionFilter($this->assetCollectionFilter); $query->setOrderings($this->orderings); return new PixxioAssetProxyQueryResult($query); } @@ -134,6 +147,7 @@ public function findBySearchTerm(string $searchTerm): AssetProxyQueryResultInter $query = new PixxioAssetProxyQuery($this->assetSource); $query->setSearchTerm($searchTerm); $query->setAssetTypeFilter($this->assetTypeFilter); + $query->setAssetCollectionFilter($this->assetCollectionFilter); $query->setOrderings($this->orderings); return new PixxioAssetProxyQueryResult($query); } @@ -147,6 +161,7 @@ public function findByTag(Tag $tag): AssetProxyQueryResultInterface $query = new PixxioAssetProxyQuery($this->assetSource); $query->setSearchTerm($tag->getLabel()); $query->setAssetTypeFilter($this->assetTypeFilter); + $query->setAssetCollectionFilter($this->assetCollectionFilter); $query->setOrderings($this->orderings); return new PixxioAssetProxyQueryResult($query); } @@ -158,6 +173,7 @@ public function findUntagged(): AssetProxyQueryResultInterface { $query = new PixxioAssetProxyQuery($this->assetSource); $query->setAssetTypeFilter($this->assetTypeFilter); + $query->setAssetCollectionFilter($this->assetCollectionFilter); $query->setOrderings($this->orderings); return new PixxioAssetProxyQueryResult($query); } diff --git a/Classes/Command/PixxioCommandController.php b/Classes/Command/PixxioCommandController.php index c6f54af..d059af3 100644 --- a/Classes/Command/PixxioCommandController.php +++ b/Classes/Command/PixxioCommandController.php @@ -9,9 +9,15 @@ use Flownative\Pixxio\Exception\MissingClientSecretException; use Neos\Flow\Annotations as Flow; use Neos\Flow\Cli\CommandController; +use Neos\Flow\Cli\Exception\StopCommandException; +use Neos\Flow\Persistence\Exception\IllegalObjectTypeException; use Neos\Media\Domain\Model\Asset; +use Neos\Media\Domain\Model\AssetCollection; use Neos\Media\Domain\Model\AssetSource\AssetSourceAwareInterface; +use Neos\Media\Domain\Repository\AssetCollectionRepository; use Neos\Media\Domain\Repository\AssetRepository; +use Neos\Media\Domain\Repository\TagRepository; +use Neos\Media\Domain\Service\AssetSourceService; class PixxioCommandController extends CommandController { @@ -21,6 +27,30 @@ class PixxioCommandController extends CommandController */ protected $assetRepository; + /** + * @Flow\Inject + * @var AssetSourceService + */ + protected $assetSourceService; + + /** + * @Flow\Inject + * @var TagRepository + */ + protected $tagRepository; + + /** + * @Flow\Inject + * @var AssetCollectionRepository + */ + protected $assetCollectionRepository; + + /** + * @Flow\InjectConfiguration(path="mapping", package="Flownative.Pixxio") + * @var array + */ + protected $mapping = []; + /** * @Flow\InjectConfiguration(path="assetSources", package="Neos.Media") * @var array @@ -194,4 +224,80 @@ public function updateMetadataCommand(string $assetSource = 'flownative-pixxio', !$quiet && $this->outputLine(' in order to make changes visible in the frontend'); } } + + /** + * Import pixx.io categories as asset collections + * + * @param string $assetSourceIdentifier Name of the pixx.io asset source (defaults to "flownative-pixxio") + * @param bool $quiet If set, only errors will be displayed. + * @param bool $dryRun If set, no changes will be made. + * @return void + * @throws IllegalObjectTypeException + * @throws \Flownative\Pixxio\Exception\ConnectionException + */ + public function importCategoriesAsCollectionsCommand(string $assetSourceIdentifier = 'flownative-pixxio', bool $quiet = true, bool $dryRun = false): void + { + !$quiet && $this->outputLine('Importing categories as asset collections via pixx.io API'); + + try { + $pixxioAssetSource = new PixxioAssetSource($assetSourceIdentifier, $this->assetSourcesConfiguration[$assetSourceIdentifier]['assetSourceOptions']); + $cantoClient = $pixxioAssetSource->getPixxioClient(); + } catch (\Exception $e) { + $this->outputLine('pixx.io client could not be created'); + $this->quit(1); + } + + $response = $cantoClient->getCategories(); + $responseObject = \GuzzleHttp\json_decode($response->getBody()); + foreach ($responseObject->categories as $categoryPath) { + $categoryPath = ltrim($categoryPath, '/'); + if ($this->shouldBeImportedAsAssetCollection($categoryPath)) { + $assetCollection = $this->assetCollectionRepository->findOneByTitle($categoryPath); + + if (!($assetCollection instanceof AssetCollection)) { + if (!$dryRun) { + $assetCollection = new AssetCollection($categoryPath); + $this->assetCollectionRepository->add($assetCollection); + } + !$quiet && $this->outputLine('+ %s', [$categoryPath]); + } else { + !$quiet && $this->outputLine('= %s', [$categoryPath]); + } + } else { + !$quiet && $this->outputLine('o %s', [$categoryPath]); + } + } + + !$quiet && $this->outputLine('Import done.'); + } + + public function shouldBeImportedAsAssetCollection(string $categoryPath): bool + { + $categoriesMapping = $this->mapping['categories']; + if (empty($categoriesMapping)) { + $this->outputLine('No categories configured for mapping'); + $this->quit(1); + } + + $categoryPath = ltrim($categoryPath, '/'); + + // depth limit + if (substr_count($categoryPath, '/') >= $this->mapping['categoriesMaximumDepth']) { + return false; + } + + // full match + if (array_key_exists($categoryPath, $categoriesMapping)) { + return $categoriesMapping[$categoryPath]['asAssetCollection']; + } + + // glob match + foreach ($categoriesMapping as $mappedCategory => $mapping) { + if (fnmatch($mappedCategory, $categoryPath)) { + return $mapping['asAssetCollection']; + } + } + + return false; + } } diff --git a/Classes/Service/PixxioClient.php b/Classes/Service/PixxioClient.php index c69c26c..92bc690 100644 --- a/Classes/Service/PixxioClient.php +++ b/Classes/Service/PixxioClient.php @@ -191,13 +191,14 @@ public function updateFile(string $id, array $metadata): ResponseInterface * @param string $queryExpression * @param array $formatTypes * @param array $fileTypes + * @param string|null $assetCollectionFilter * @param int $offset * @param int $limit * @param array $orderings * @return ResponseInterface * @throws ConnectionException */ - public function search(string $queryExpression, array $formatTypes, array $fileTypes, int $offset = 0, int $limit = 50, $orderings = []): ResponseInterface + public function search(string $queryExpression, array $formatTypes, array $fileTypes, string $assetCollectionFilter = null, int $offset = 0, int $limit = 50, $orderings = []): ResponseInterface { $options = new \stdClass(); $options->pagination = $limit . '-' . (int)($offset / $limit + 1); @@ -206,6 +207,10 @@ public function search(string $queryExpression, array $formatTypes, array $fileT $options->formatType = $formatTypes; $options->fileType = implode(',', $fileTypes); + if ($assetCollectionFilter !== null) { + $options->category = 'sub/' . $assetCollectionFilter; + } + if (!empty($queryExpression)) { $options->searchTerm = urlencode($queryExpression); } @@ -234,6 +239,25 @@ public function search(string $queryExpression, array $formatTypes, array $fileT } } + /** + * @return ResponseInterface + * @throws ConnectionException + */ + public function getCategories(): ResponseInterface + { + $uri = new Uri( $this->apiEndpointUri . '/json/categories'); + $uri = $uri->withQuery( + 'accessToken=' . $this->accessToken + ); + + $client = new Client($this->apiClientOptions); + try { + return $client->request('GET', $uri); + } catch (GuzzleException $e) { + throw new ConnectionException('Retrieving categories failed: ' . $e->getMessage(), 1642430939); + } + } + /** * @return string */ From d470c641e697248b90950a505bb55e2afd372b06 Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Wed, 2 Feb 2022 21:47:36 +0100 Subject: [PATCH 3/5] Add command to import categories as asset collections --- Configuration/Settings.yaml | 7 +++ README.md | 53 ++++++++++++++++++++ Tests/Unit/PixxioCommandControllerTest.php | 56 ++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 Tests/Unit/PixxioCommandControllerTest.php diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index a9a32f9..ab9824d 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -1,3 +1,10 @@ +Flownative: + Pixxio: + mapping: + # map "categories" from pixx.io to Neos + categoriesMaximumDepth: 10 + categories: [] + Neos: Flow: mvc: diff --git a/README.md b/README.md index caffeae..13d5f72 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,59 @@ images will not be untagged in the Pixxio media library. Note: At this point, the auto-tagging feature is not really optimized for performance. The command merely iterates over all assets which were imported from Pixxio and checks if tags need to be updated. +### Category mapping from pixx.io to Neos + +Pixx.io offers categories to organize assets in a folder-like structure. Those +can be mapped to asset collections and tags in Neos, to make them visible for +the users. + +The configuration for this looks like that: + +```yaml +Flownative: + Pixxio: + mapping: + # map "categories" from pixx.io to Neos + categoriesMaximumDepth: 2 # only include the first two levels of categories + categories: + 'People*': # the category "path" in pixx.io, shell-style globbing is supported + asAssetCollection: true # map to an asset collection named after the category + 'People/Employees': + asAssetCollection: false # ignore this category +``` + +- The key used is the category identifier from pixx.io as used in the API +- `asAssetCollection` set to `true` exposes the category as an asset + collection named like the category. + +Afterwards, run the following command to update the asset collections, ideally +in a cronjob to keep things up-to-date: + +``` +./flow pixxio:importcategoriesascollections +``` + +To check what a given category would import, you can use a verbose dry-run: + +``` +beach@3bd503e8b61d:/application$ ./flow pixxio:importcategoriesascollections --quiet 0 --dry-run 1 +Importing categories as asset collections via pixx.io API +o Dokumentation += Kunde A += Kunde A/Projekt 1 +o Kunde A/Projekt 1/Copy +o Kunde A/Projekt 1/Design ++ Kunde A/Projekt 2 +o Kunde A/Projekt 2/Copy +o Kunde A/Projekt 2/Design +o Marketing +o home +o home/Doe_John +Import done. +``` +The above would have added "Kunde A/Projekt 2", the items marked "=" exist already, +and everything else is ignored. + ## Background and Demo [![Background and Demo](https://img.youtube.com/vi/nG05nn-Yd0I/0.jpg)](https://www.youtube.com/watch?v=nG05nn-Yd0I) diff --git a/Tests/Unit/PixxioCommandControllerTest.php b/Tests/Unit/PixxioCommandControllerTest.php new file mode 100644 index 0000000..b497861 --- /dev/null +++ b/Tests/Unit/PixxioCommandControllerTest.php @@ -0,0 +1,56 @@ + 2, + 'categories' => [ + 'home*' => ['asAssetCollection' => false], + 'Kunde A*' => ['asAssetCollection' => false], + '*' => ['asAssetCollection' => true] + ], + ]; + + public function setUp(): void + { + $this->mockCommandController = $this->getAccessibleMock(PixxioCommandController::class, ['dummy']); + $this->mockCommandController->_set('mapping', self::$mapping); + } + + public function categoriesMappingProvider(): array + { + return [ + '/home' => ['/home', false], + '/home/foo' => ['/home/foo', false], + '/Kunde A' => ['/Kunde A', false], + '/Kunde A/Projekt 1' => ['/Kunde A/Projekt 1', false], + '/Kunde A/Projekt 1/Design' => ['/Kunde A/Projekt 1/Design', false], + '/Kunde A/Projekt 1/Copy' => ['/Kunde A/Projekt 1/Copy', false], + '/Kunde B' => ['/Kunde B', true], + '/Kunde B/Projekt 1' => ['/Kunde B/Projekt 1', true], + '/Kunde B/Projekt 1/Design' => ['/Kunde B/Projekt 1/Design', false], + '/Kunde B/Projekt 1/Copy' => ['/Kunde B/Projekt 1/Copy', false], + '/Kunde B/Projekt 2' => ['/Kunde B/Projekt 2', true], + '/Kunde B/Projekt 2/Copy' => ['/Kunde B/Projekt 2/Copy', false], + '/Kunde B/Projekt 2/Design' => ['/Kunde B/Projekt 2/Design', false], + '/Marketing' => ['/Marketing', true], + ]; + } + + /** + * @test + * @dataProvider categoriesMappingProvider + */ + public function shouldBeImportedAsAssetCollectionWorks(string $category, bool $expected): void + { + self::assertSame($expected, $this->mockCommandController->_call('shouldBeImportedAsAssetCollection', $category), $category); + } +} From 6d454d9507b37b9b588599f4e5c8205db31a54df Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Thu, 3 Feb 2022 09:53:28 +0100 Subject: [PATCH 4/5] Tweak README, note about Neos version requirements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … for the category support. --- README.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 13d5f72..a7352b0 100644 --- a/README.md +++ b/README.md @@ -190,11 +190,20 @@ iterates over all assets which were imported from Pixxio and checks if tags need ### Category mapping from pixx.io to Neos -Pixx.io offers categories to organize assets in a folder-like structure. Those +pixx.io offers categories to organize assets in a folder-like structure. Those can be mapped to asset collections and tags in Neos, to make them visible for the users. -The configuration for this looks like that: +.. note:: + The pixx.io asset source declares itself read-only. Neos does not show asset + collections in the UI for read-only asset sources. This has been changed for + Neos 7.3.0 and up with https://github.com/neos/neos-development-collection/pull/3481 + + If you want to use this feature with older Neos versions, you can use the PR with + [cweagans/composer-patches](https://github.com/cweagans/composer-patches#readme) + or copy the adjusted template into your project and use `Views.yaml` to activate it. + +The configuration for the category import looks like this: ```yaml Flownative: @@ -203,27 +212,28 @@ Flownative: # map "categories" from pixx.io to Neos categoriesMaximumDepth: 2 # only include the first two levels of categories categories: + 'People/Employees': + asAssetCollection: false # ignore this category, put more specific patterns first 'People*': # the category "path" in pixx.io, shell-style globbing is supported asAssetCollection: true # map to an asset collection named after the category - 'People/Employees': - asAssetCollection: false # ignore this category ``` -- The key used is the category identifier from pixx.io as used in the API +- The key used is the category identifier from pixx.io as used in the API, + without leading slash - `asAssetCollection` set to `true` exposes the category as an asset collection named like the category. Afterwards, run the following command to update the asset collections, ideally in a cronjob to keep things up-to-date: -``` +```bash ./flow pixxio:importcategoriesascollections ``` To check what a given category would import, you can use a verbose dry-run: -``` -beach@3bd503e8b61d:/application$ ./flow pixxio:importcategoriesascollections --quiet 0 --dry-run 1 +```bash +$ ./flow pixxio:importcategoriesascollections --quiet 0 --dry-run 1 Importing categories as asset collections via pixx.io API o Dokumentation = Kunde A From 94237da70e452e72a4384dbf2c920f893ff301e9 Mon Sep 17 00:00:00 2001 From: Karsten Dambekalns Date: Wed, 16 Feb 2022 10:28:53 +0100 Subject: [PATCH 5/5] Tweak note blocks in README --- .editorconfig | 2 ++ README.md | 21 +++++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..16a8552 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,2 @@ +[*.md] +trim_trailing_whitespace = false diff --git a/README.md b/README.md index 745786c..13fbbc9 100644 --- a/README.md +++ b/README.md @@ -179,8 +179,11 @@ It is recommended to run this command through a cron-job, ideally in combination command. It's important to run the `removeunused`-command *after* the tagging command, because otherwise removed images will not be untagged in the pixx.io media library. -Note: At this point, the auto-tagging feature is not really optimized for performance. The command merely +--- +**NOTE** +At this point, the auto-tagging feature is not really optimized for performance. The command merely iterates over all assets which were imported from pixx.io and checks if tags need to be updated. +--- ### Category mapping from pixx.io to Neos @@ -188,14 +191,16 @@ pixx.io offers categories to organize assets in a folder-like structure. Those can be mapped to asset collections and tags in Neos, to make them visible for the users. -.. note:: - The pixx.io asset source declares itself read-only. Neos does not show asset - collections in the UI for read-only asset sources. This has been changed for - Neos 7.3.0 and up with https://github.com/neos/neos-development-collection/pull/3481 +--- +**NOTE** +The pixx.io asset source declares itself read-only. Neos does not show asset +collections in the UI for read-only asset sources. This has been changed for +Neos 7.3.0 and up with https://github.com/neos/neos-development-collection/pull/3481 - If you want to use this feature with older Neos versions, you can use the PR with - [cweagans/composer-patches](https://github.com/cweagans/composer-patches#readme) - or copy the adjusted template into your project and use `Views.yaml` to activate it. + If you want to use this feature with older Neos versions, you can use the PR with + [cweagans/composer-patches](https://github.com/cweagans/composer-patches#readme) + or copy the adjusted template into your project and use `Views.yaml` to activate it. +--- The configuration for the category import looks like this: