diff --git a/.gitignore b/.gitignore
index 7595791..a010026 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,6 @@
.prettierrc
package.json
yarn.lock
+composer.lock
+/public/*
+/vendor/*
diff --git a/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php b/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php
index 5d4cac1..020432b 100644
--- a/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php
+++ b/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php
@@ -12,19 +12,14 @@
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
-use Visol\Cloudinary\Driver\CloudinaryFastDriver;
+use Visol\Cloudinary\Driver\CloudinaryDriver;
use Visol\Cloudinary\Services\ConfigurationService;
class InlineCloudinaryControlContainer extends InlineControlContainer
{
- public function render() {
-
- // We load here the cloudinary library
- /** @var AssetCollector $assetCollector */
- $assetCollector = GeneralUtility::makeInstance(AssetCollector::class);
- $assetCollector->addJavaScript('media_library_cloudinary', 'https://media-library.cloudinary.com/global/all.js', []);
-
+ public function render()
+ {
/** @var PageRenderer $pageRenderer */
$pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
$pageRenderer->loadRequireJsModule('TYPO3/CMS/Cloudinary/CloudinaryMediaLibrary');
@@ -32,30 +27,13 @@ public function render() {
return parent::render();
}
- /**
- * @param array $inlineConfiguration
- * @return string
- */
- protected function renderPossibleRecordsSelectorTypeGroupDB(array $inlineConfiguration)
+ protected function renderPossibleRecordsSelectorTypeGroupDB(array $inlineConfiguration): ?string
{
$typo3Buttons = parent::renderPossibleRecordsSelectorTypeGroupDB($inlineConfiguration);
// We could have multiple cloudinary buttons / storages
$cloudinaryButtons = $this->renderCloudinaryButtons($inlineConfiguration);
-
- // Inject button before help-block
- if (strpos($typo3Buttons, '
') > 0) {
- $typo3Buttons = str_replace(
- '
',
- $cloudinaryButtons . '
',
- $typo3Buttons,
- );
- // Try to inject it into the form-control container
- } elseif (preg_match('/<\/div><\/div>$/i', $typo3Buttons)) {
- $typo3Buttons = preg_replace('/<\/div><\/div>$/i', $cloudinaryButtons . '
', $typo3Buttons);
- } else {
- $typo3Buttons .= $cloudinaryButtons;
- }
+ $typo3Buttons = $this->appendButtons($typo3Buttons, $cloudinaryButtons);
return $typo3Buttons;
}
@@ -72,7 +50,7 @@ protected function renderCloudinaryButtons(array $inlineConfiguration): string
$view = $this->initializeStandaloneView('EXT:cloudinary/Resources/Private/Standalone/MediaLibrary/Show.html');
$view->assignMultiple([
'objectGroup' => $objectGroup,
- 'cloudinaryCredentials' => json_encode($this->computeCloudinaryCredentials($storages)),
+ 'cloudinaryCredentials' => $this->computeCloudinaryCredentials($storages),
]);
return $view->render();
@@ -107,7 +85,7 @@ protected function getCloudinaryStorages(): array
$storageItems = $query
->select('*')
->from('sys_file_storage')
- ->where($query->expr()->eq('driver', $query->expr()->literal(CloudinaryFastDriver::DRIVER_TYPE)))
+ ->where($query->expr()->eq('driver', $query->expr()->literal(CloudinaryDriver::DRIVER_TYPE)))
->execute()
->fetchAllAssociativeIndexed();
@@ -159,4 +137,22 @@ protected function computeCloudinaryCredentials(array $storages): array
return $cloudinaryCredentials;
}
+
+ protected function appendButtons(string $typo3Buttons, string $cloudinaryButtons): ?string
+ {
+ // Inject button before help-block
+ if (strpos($typo3Buttons, '') > 0) {
+ $typo3Buttons = str_replace(
+ '
',
+ $cloudinaryButtons . '
',
+ $typo3Buttons,
+ );
+ // Try to inject it into the form-control container
+ } elseif (preg_match('/<\/div><\/div>$/i', $typo3Buttons)) {
+ $typo3Buttons = preg_replace('/<\/div><\/div>$/i', $cloudinaryButtons . '
', $typo3Buttons);
+ } else {
+ $typo3Buttons .= $cloudinaryButtons;
+ }
+ return $typo3Buttons;
+ }
}
diff --git a/Classes/Cache/CloudinaryTypo3Cache.php b/Classes/Cache/CloudinaryTypo3Cache.php
deleted file mode 100644
index eba3d91..0000000
--- a/Classes/Cache/CloudinaryTypo3Cache.php
+++ /dev/null
@@ -1,189 +0,0 @@
-storageUid = $storageUid;
- }
-
- /**
- * @param string $folderIdentifier
- * @return array|false
- */
- public function getCachedFiles(string $folderIdentifier)
- {
- return $this->get($this->computeFileCacheKey($folderIdentifier));
- }
-
- /**
- * @param string $folderIdentifier
- * @param array $files
- */
- public function setCachedFiles(string $folderIdentifier, array $files): void
- {
- $this->set($this->computeFileCacheKey($folderIdentifier), $files, self::TAG_FILE);
- }
-
- /**
- * @param string $folderIdentifier
- * @return array|false
- */
- public function getCachedFolders(string $folderIdentifier)
- {
- return $this->get($this->computeFolderCacheKey($folderIdentifier));
- }
-
- /**
- * @param string $folderIdentifier
- * @param array $folders
- */
- public function setCachedFolders(string $folderIdentifier, array $folders): void
- {
- $this->set($this->computeFolderCacheKey($folderIdentifier), $folders, self::TAG_FOLDER);
- }
-
- /**
- * @param string $identifier
- * @return array|false
- */
- protected function get(string $identifier)
- {
- return $this->isCacheEnabled ? $this->getCacheInstance()->get($identifier) : false;
- }
-
- /**
- * @param string $identifier
- * @param array $data
- * @param string $tag
- */
- protected function set(string $identifier, array $data, $tag): void
- {
- if ($this->isCacheEnabled) {
- $this->getCacheInstance()->set($identifier, $data, [$tag], self::LIFETIME);
-
- $this->log('Caching "%s" data with folder identifier "%s"', [$tag, $identifier]);
- }
- }
-
- /**
- * @param string $folderIdentifier
- * @return mixed
- */
- protected function computeFolderCacheKey($folderIdentifier): string
- {
- // Sanitize the cache format as the key can not contains certain characters such as "/", ":", etc..
- return sprintf('storage-%s-folders-%s', $this->storageUid, str_replace('/', '%', $folderIdentifier));
- }
-
- /**
- * @param string $folderIdentifier
- * @return mixed
- */
- protected function computeFileCacheKey($folderIdentifier): string
- {
- // Sanitize the cache format as the key can not contains certain characters such as "/", ":", etc..
- return sprintf('storage-%s-files-%s', $this->storageUid, str_replace('/', '%', $folderIdentifier));
- }
-
- /**
- * @return void
- */
- public function flushFileCache(): void
- {
- $this->getCacheInstance()->flushByTags([self::TAG_FILE]);
- $this->log('Method "flushFileCache": file cache flushed');
- }
-
- /**
- * @return void
- */
- public function flushFolderCache(): void
- {
- $this->getCacheInstance()->flushByTags([self::TAG_FOLDER]);
- $this->log('Method "flushFolderCache": folder cache flushed');
- }
-
- /**
- * @return void
- */
- public function flushAll(): void
- {
- $this->getCacheInstance()->flush();
- $this->log('Method "flushAll": all cache flushed');
- }
-
- /**
- * @return AbstractFrontend
- */
- protected function getCacheInstance()
- {
- return $this->getCacheManager()->getCache('cloudinary');
- }
-
- /**
- * Return the Cache Manager
- *
- * @return CacheManager|object
- */
- protected function getCacheManager()
- {
- return GeneralUtility::makeInstance(CacheManager::class);
- }
-
- /**
- * @param string $message
- * @param array $arguments
- */
- public function log(string $message, array $arguments = [])
- {
- /** @var Logger $logger */
- $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
- #$logger->log(
- # LogLevel::INFO,
- # vsprintf('[CACHE] ' . $message, $arguments)
- #);
- }
-}
diff --git a/Classes/CloudinaryException.php b/Classes/CloudinaryException.php
index 6acc908..f91291b 100644
--- a/Classes/CloudinaryException.php
+++ b/Classes/CloudinaryException.php
@@ -2,6 +2,8 @@
namespace Visol\Cloudinary;
-class CloudinaryException extends \Exception
+use Exception;
+
+class CloudinaryException extends Exception
{
}
diff --git a/Classes/CloudinaryFactory.php b/Classes/CloudinaryFactory.php
index c84992d..2b8f0d7 100644
--- a/Classes/CloudinaryFactory.php
+++ b/Classes/CloudinaryFactory.php
@@ -9,6 +9,7 @@
namespace Visol\Cloudinary;
+use Exception;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Resource\ResourceFactory;
@@ -18,7 +19,7 @@
/**
* Class CloudinaryFactory
*/
-class CloudinaryFactory extends \Exception
+class CloudinaryFactory extends Exception
{
/**
* @return ResourceStorage
diff --git a/Classes/Command/AbstractCloudinaryCommand.php b/Classes/Command/AbstractCloudinaryCommand.php
index 6a309fc..00948f2 100644
--- a/Classes/Command/AbstractCloudinaryCommand.php
+++ b/Classes/Command/AbstractCloudinaryCommand.php
@@ -29,30 +29,15 @@ abstract class AbstractCloudinaryCommand extends Command
const WARNING = 'warning';
const ERROR = 'error';
- /**
- * @var SymfonyStyle
- */
- protected $io;
+ protected SymfonyStyle $io;
- /**
- * @var bool
- */
- protected $isSilent = false;
+ protected bool $isSilent = false;
- /**
- * @var string
- */
- protected $tableName = 'sys_file';
+ protected string $tableName = 'sys_file';
- /**
- * @param ResourceStorage $storage
- * @param InputInterface $input
- *
- * @return array
- */
protected function getFiles(ResourceStorage $storage, InputInterface $input): array
{
- $query = $this->getQueryBuilder();
+ $query = $this->getQueryBuilder($this->tableName);
$query
->select('*')
->from($this->tableName)
@@ -110,13 +95,9 @@ protected function getFiles(ResourceStorage $storage, InputInterface $input): ar
}
}
- return $query->execute()->fetchAll();
+ return $query->execute()->fetchAllAssociative();
}
- /**
- * @param string $type
- * @param array $files
- */
protected function writeLog(string $type, array $files)
{
$logFileName = sprintf(
@@ -141,22 +122,15 @@ protected function writeLog(string $type, array $files)
);
}
- /**
- * @param ResourceStorage $storage
- *
- * @return bool
- */
protected function checkDriverType(ResourceStorage $storage): bool
{
return $storage->getDriverType() === CloudinaryDriver::DRIVER_TYPE;
}
/**
- * @param string $message
- * @param array $arguments
* @param string $severity can be 'warning', 'error', 'success'
*/
- protected function log(string $message = '', array $arguments = [], $severity = '')
+ protected function log(string $message = '', array $arguments = [], string $severity = '')
{
if (!$this->isSilent) {
$formattedMessage = vsprintf($message, $arguments);
@@ -168,47 +142,28 @@ protected function log(string $message = '', array $arguments = [], $severity =
}
}
- /**
- * @param string $message
- * @param array $arguments
- */
protected function success(string $message = '', array $arguments = [])
{
$this->log($message, $arguments, self::SUCCESS);
}
- /**
- * @param string $message
- * @param array $arguments
- */
protected function warning(string $message = '', array $arguments = [])
{
$this->log($message, $arguments, self::WARNING);
}
- /**
- * @param string $message
- * @param array $arguments
- */
protected function error(string $message = '', array $arguments = [])
{
$this->log($message, $arguments, self::ERROR);
}
-
- /**
- * @return object|QueryBuilder
- */
- protected function getQueryBuilder(): QueryBuilder
+ protected function getQueryBuilder(string $tableName): QueryBuilder
{
/** @var ConnectionPool $connectionPool */
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
- return $connectionPool->getQueryBuilderForTable($this->tableName);
+ return $connectionPool->getQueryBuilderForTable($tableName);
}
- /**
- * @return object|Connection
- */
protected function getConnection(): Connection
{
/** @var ConnectionPool $connectionPool */
diff --git a/Classes/Command/CloudinaryAcceptanceTestCommand.php b/Classes/Command/CloudinaryAcceptanceTestCommand.php
index ae8cac6..69631ca 100644
--- a/Classes/Command/CloudinaryAcceptanceTestCommand.php
+++ b/Classes/Command/CloudinaryAcceptanceTestCommand.php
@@ -9,7 +9,9 @@
* LICENSE.md file that was distributed with this source code.
*/
+use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Style\SymfonyStyle;
+use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Database\ConnectionPool;
use Visol\Cloudinary\Driver\CloudinaryDriver;
use Symfony\Component\Console\Input\InputArgument;
@@ -41,15 +43,12 @@ function ($className) {
}
);
-/**
- * Class CloudinaryAcceptanceTestCommand
- */
class CloudinaryAcceptanceTestCommand extends AbstractCloudinaryCommand
{
/**
* Configure the command by defining the name, options and arguments
*/
- protected function configure()
+ protected function configure(): void
{
$message = 'Run a suite of Acceptance Tests';
$this
@@ -62,54 +61,39 @@ protected function configure()
'The API configuration'
)
->setHelp(
- 'Usage: ./vendor/bin/typo3 cloudinary:tests'
+ 'Usage: ./vendor/bin/typo3 cloudinary:tests bucket-name:my-api-key:my-api-secret'
);
}
- /**
- * @param InputInterface $input
- * @param OutputInterface $output
- */
- protected function initialize(InputInterface $input, OutputInterface $output)
+ protected function initialize(InputInterface $input, OutputInterface $output): void
{
$this->io = new SymfonyStyle($input, $output);
}
- /**
- * @param InputInterface $input
- * @param OutputInterface $output
- *
- * @return int
- */
protected function execute(InputInterface $input, OutputInterface $output): int
{
// We should dynamically inject the configuration. For now use an existing driver
[$couldName, $apiKey, $apiSecret] = GeneralUtility::trimExplode(':', $input->getArgument('api-configuration'));
if (!$couldName || !$apiKey || !$apiSecret) {
// Everything must be defined!
- $message = 'API configuration is incomplete. Format should be "cld-name:1234:abcd".' . LF . LF;
- $message .= '"cld-name" is the name of the cloudinary bucket' . LF;
- $message .= '"12345" is the API key' . LF;
- $message .= '"abcd" is the API secret' . LF . LF;
- $message .= 'https://cloudinary.com/console' . LF;
- $message .= 'Strong advice! Take a free account to run the test suite';
+ $message = 'API configuration is incomplete. Format should be "bucket-name:my-api-key:my-api-secret".';
$this->error($message);
- return 1;
+ return Command::INVALID;
}
- $this->log('Starting tests...');
+ $logFile = Environment::getVarPath() . '/log/cloudinary.log';
$this->log('Hint! Look at the log to get more insight:');
- $this->log('tail -f web/typo3temp/var/logs/cloudinary.log');
+ $this->log('tail -f ' . $logFile);
$this->log();
// Create a testing storage
$storageId = $this->setUp($couldName, $apiKey, $apiSecret);
if (!$storageId) {
$this->error('Something went wrong. I could not create a testing storage');
- return 2;
+ return Command::FAILURE;
}
- // Test case for video file
+ // Test case for files
$testSuite = new FileTestSuite($storageId, $this->io);
$testSuite->runTests();
@@ -118,16 +102,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->tearDown($storageId);
- return 0;
+ return Command::SUCCESS;
}
- /**
- * @param string $cloudName
- * @param string $apiKey
- * @param string $apiSecret
- *
- * @return int
- */
protected function setUp(string $cloudName, string $apiKey, string $apiSecret): int
{
$values = [
@@ -178,9 +155,6 @@ protected function setUp(string $cloudName, string $apiKey, string $apiSecret):
return (int)$db->lastInsertId();
}
- /**
- * @param int $storageId
- */
protected function tearDown(int $storageId)
{
/** @var ConnectionPool $connectionPool */
@@ -205,7 +179,7 @@ protected function tearDown(int $storageId)
);
// Remove all cache
- $db->truncate('cf_cloudinary');
- $db->truncate('cf_cloudinary_tags');
+// $db->truncate('cf_cloudinary');
+// $db->truncate('cf_cloudinary_tags');
}
}
diff --git a/Classes/Command/CloudinaryApiCommand.php b/Classes/Command/CloudinaryApiCommand.php
new file mode 100644
index 0000000..d2c2860
--- /dev/null
+++ b/Classes/Command/CloudinaryApiCommand.php
@@ -0,0 +1,190 @@
+1d"
+
+# List the resources instead of the whole resource
+typo3 cloudinary:api [0-9] --expression="folder=fileadmin/_processed_/*" --list
+
+# Delete the resources according to the expression
+typo3 cloudinary:api [0-9] --expression="folder=fileadmin/_processed_/*" --delete
+typo3 cloudinary:api [0-9] --expression="folder=fileadmin/_processed_/*" --delete --force
+ ';
+
+ protected function initialize(InputInterface $input, OutputInterface $output): void
+ {
+ $this->io = new SymfonyStyle($input, $output);
+
+ /** @var ResourceFactory $resourceFactory */
+ $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
+ $this->storage = $resourceFactory->getStorageObject($input->getArgument('storage'));
+ }
+
+ protected function configure(): void
+ {
+ $message = 'Interact with cloudinary API';
+ $this->setDescription($message)
+ ->addOption('silent', 's', InputOption::VALUE_OPTIONAL, 'Mute output as much as possible', false)
+ ->addOption('force', 'f', InputOption::VALUE_OPTIONAL, 'Force the given action without further confirmation', false)
+ ->addOption('fileUid', '', InputOption::VALUE_OPTIONAL, 'File uid', '')
+ ->addOption('publicId', '', InputOption::VALUE_OPTIONAL, 'Cloudinary public id', '')
+ ->addOption('type', '', InputOption::VALUE_OPTIONAL, 'In combination with publicId, overrides the type. Possible value iamge, video or raw', 'image')
+ ->addOption('expression', '', InputOption::VALUE_OPTIONAL, 'Cloudinary search expression e.g --expression="folder=fileadmin/*"', '')
+ ->addOption('list', '', InputOption::VALUE_OPTIONAL, 'List instead of the whole resource --expression="folder=fileadmin/_processed_/*" --list', false)
+ ->addOption('delete', '', InputOption::VALUE_OPTIONAL, 'Delete the resources --expression="folder=fileadmin/*" --delete', false)
+ ->addArgument('storage', InputArgument::OPTIONAL, 'Storage identifier')
+ ->setHelp($this->help);
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ if (!$this->checkDriverType($this->storage)) {
+ $this->log('Look out! Storage is not of type "cloudinary"');
+ return Command::INVALID;
+ }
+
+ $publicId = $input->getOption('publicId');
+ $expression = $input->getOption('expression');
+ $list = $input->getOption('list') === null;
+ $delete = $input->getOption('delete') === null;
+ $force = $input->getOption('force') === null;
+
+ if ($delete && !$force) {
+ // ask the user whether it should continue
+ $continue = $this->io->confirm('Are you sure you want to delete the resources?');
+ if (!$continue) {
+ $this->log('Aborting...');
+ return Command::SUCCESS;
+ }
+ }
+
+ /** @var int $fileUid */
+ $fileUid = $input->getOption('fileUid');
+ if ($fileUid) {
+ /** @var ResourceFactory $resourceFactory */
+ $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
+ $file = $resourceFactory->getFileObject($fileUid);
+
+ $this->storage = $file->getStorage(); // just to be sure
+ $publicId = $this->getPublicIdFromFile($file);
+ }
+
+ try {
+ if ($publicId) {
+ $resource = $this->getAdminApi()->asset($publicId, ['resource_type' => $input->getOption('type')]);
+ $this->log(var_export((array)$resource, true));
+ } elseif ($expression) {
+
+ $counter = 0;
+ do {
+ $nextCursor = isset($response)
+ ? $response['next_cursor']
+ : '';
+
+ $response = $this->getSearchApi()
+ ->expression($expression)
+ ->sortBy('public_id', 'asc')
+ ->maxResults(100)
+ ->nextCursor($nextCursor)
+ ->execute();
+
+ if (is_array($response['resources'])) {
+ $_resources = [];
+ foreach ($response['resources'] as $resource) {
+ if ($list || $delete) {
+ $this->log($resource['public_id']);
+ } else {
+ $this->log(var_export((array)$resource, true));
+ }
+
+ // collect resources in case of deletion.
+ $_resources[] = $resource['public_id'];
+ }
+ // delete the resource if told
+ if ($delete) {
+ $counter++;
+ $this->log("\nDeleting batch #$counter...\n");
+ $this->getAdminApi()->deleteAssets($_resources);
+ }
+ }
+ } while (!empty($response) && isset($response['next_cursor']));
+
+ } else {
+ $this->log('Nothing to do...');
+ }
+ } catch (Exception $exception) {
+ // Triggered when no resources are found when deleting files.
+ if ($exception->getMessage() === 'Missing required parameter - public_ids') {
+ $this->log("No resources found for expression '$expression'. Nothing to do...");
+ return Command::SUCCESS;
+ }
+ $this->error($exception->getMessage());
+ }
+
+ return Command::SUCCESS;
+ }
+
+ protected function getPublicIdFromFile(File $file): string
+ {
+ /** @var CloudinaryPathService $cloudinaryPathService */
+ $cloudinaryPathService = GeneralUtility::makeInstance(
+ CloudinaryPathService::class,
+ $file->getStorage(),
+ );
+ return $cloudinaryPathService->computeCloudinaryPublicId($file->getIdentifier());
+ }
+
+ protected function getSearchApi(): SearchApi
+ {
+ return CloudinaryApiUtility::getCloudinary($this->storage)->searchApi();
+ }
+
+ protected function getAdminApi(): AdminApi
+ {
+ return CloudinaryApiUtility::getCloudinary($this->storage)->adminApi();
+ }
+
+}
diff --git a/Classes/Command/CloudinaryCopyCommand.php b/Classes/Command/CloudinaryCopyCommand.php
index dba3ca2..f9b5b2e 100644
--- a/Classes/Command/CloudinaryCopyCommand.php
+++ b/Classes/Command/CloudinaryCopyCommand.php
@@ -9,6 +9,7 @@
* LICENSE.md file that was distributed with this source code.
*/
+use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -20,31 +21,15 @@
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
-/**
- * Class CloudinaryCopyCommand
- */
class CloudinaryCopyCommand extends AbstractCloudinaryCommand
{
- /**
- * @var array
- */
- protected $missingFiles = [];
+ protected array $missingFiles = [];
- /**
- * @var ResourceStorage
- */
- protected $sourceStorage;
+ protected ResourceStorage $sourceStorage;
- /**
- * @var ResourceStorage
- */
- protected $targetStorage;
+ protected ResourceStorage $targetStorage;
- /**
- * @param InputInterface $input
- * @param OutputInterface $output
- */
- protected function initialize(InputInterface $input, OutputInterface $output)
+ protected function initialize(InputInterface $input, OutputInterface $output): void
{
$this->io = new SymfonyStyle($input, $output);
@@ -59,7 +44,7 @@ protected function initialize(InputInterface $input, OutputInterface $output)
/**
* Configure the command by defining the name, options and arguments
*/
- protected function configure()
+ protected function configure(): void
{
$this->setDescription('Copy bunch of images from a local storage to a cloudinary storage')
->addOption('silent', 's', InputOption::VALUE_OPTIONAL, 'Mute output as much as possible', false)
@@ -74,22 +59,18 @@ protected function configure()
->setHelp('Usage: ./vendor/bin/typo3 cloudinary:copy 1 2');
}
- /**
- * @param InputInterface $input
- * @param OutputInterface $output
- */
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (!$this->checkDriverType($this->targetStorage)) {
$this->log('Look out! target storage is not of type "cloudinary"');
- return 1;
+ return Command::INVALID;
}
$files = $this->getFiles($this->sourceStorage, $input);
if (count($files) === 0) {
$this->log('No files found, no work for me!');
- return 0;
+ return Command::SUCCESS;
}
$this->log('Copying %s files from storage "%s" (%s) to "%s" (%s)', [
@@ -106,7 +87,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if (!$response) {
$this->log('Script aborted');
- return 0;
+ return Command::SUCCESS;
}
}
@@ -148,28 +129,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int
print_r($this->missingFiles);
}
- return 0;
+ return Command::SUCCESS;
}
- /**
- * @param File $fileObject
- * @param string $url
- *
- * @return bool
- */
public function download(File $fileObject, string $url): bool
{
$this->ensureDirectoryExistence($fileObject);
$contents = file_get_contents($url);
- return $contents ? (bool) file_put_contents($this->getAbsolutePath($fileObject), $contents) : false;
+ return $contents ? (bool)file_put_contents($this->getAbsolutePath($fileObject), $contents) : false;
}
- /**
- * @param File $fileObject
- *
- * @return string
- */
protected function getAbsolutePath(File $fileObject): string
{
// Compute the absolute file name of the file to move
@@ -178,9 +148,6 @@ protected function getAbsolutePath(File $fileObject): string
return GeneralUtility::getFileAbsFileName($fileRelativePath);
}
- /**
- * @param File $fileObject
- */
protected function ensureDirectoryExistence(File $fileObject)
{
// Make sure the directory exists
diff --git a/Classes/Command/CloudinaryFixImageDimensionCommand.php b/Classes/Command/CloudinaryFixImageDimensionCommand.php
new file mode 100644
index 0000000..0a78bbf
--- /dev/null
+++ b/Classes/Command/CloudinaryFixImageDimensionCommand.php
@@ -0,0 +1,146 @@
+io = new SymfonyStyle($input, $output);
+
+ $this->isSilent = $input->getOption('silent');
+
+ /** @var ResourceFactory $resourceFactory */
+ $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
+ $this->storage = $resourceFactory->getStorageObject($input->getArgument('target'));
+
+ $this->cloudinaryResourceService = GeneralUtility::makeInstance(
+ CloudinaryResourceService::class,
+ $this->storage,
+ );
+
+ $this->cloudinaryPathService = GeneralUtility::makeInstance(
+ CloudinaryPathService::class,
+ $this->storage,
+ );
+
+ $this->metadataRepository = GeneralUtility::makeInstance(MetaDataRepository::class);
+ }
+
+ protected function configure(): void
+ {
+ $this->setDescription('Fix missing width and height of image.')
+ ->addOption('silent', 's', InputOption::VALUE_OPTIONAL, 'Mute output as much as possible', false)
+ ->addOption('yes', 'y', InputOption::VALUE_OPTIONAL, 'Accept everything by default', false)
+ ->addArgument('target', InputArgument::REQUIRED, 'Target storage identifier')
+ ->setHelp('Usage: ./vendor/bin/typo3 cloudinary:fix:dimension [0-9]');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ if (!$this->checkDriverType($this->storage)) {
+ $this->log('Look out! target storage is not of type "cloudinary"');
+ return Command::INVALID;
+ }
+
+ $files = $this->getProblematicImages();
+
+ foreach ($files as $file) {
+ $fileObject = GeneralUtility::makeInstance(ResourceFactory::class)->getFileObject($file['uid']);
+ if ($fileObject->exists()) {
+ $this->log('Fixing %s (%s)', [$fileObject->getIdentifier(), $fileObject->getUid()]);
+
+ // get the corresponding cloudinary resource
+ $publicId = $this->cloudinaryPathService->computeCloudinaryPublicId($fileObject->getIdentifier());
+
+ $cloudinaryResource = $this->cloudinaryResourceService->getResource($publicId);
+
+ // we update the metadata
+ $this->metadataRepository->update(
+ $fileObject->getUid(),
+ [
+ 'width' => $cloudinaryResource['width'],
+ 'height' => $cloudinaryResource['height'],
+ ]
+ );
+
+ }
+ }
+
+ return Command::SUCCESS;
+ }
+
+ protected function getProblematicImages(): array
+ {
+ $query = $this->getQueryBuilder($this->tableName);
+ $query
+ ->select('sys_file.uid')
+ ->from($this->tableName)
+ ->join(
+ $this->tableName,
+ 'sys_file_metadata',
+ 'metadata',
+ $query->expr()->eq(
+ 'metadata.file',
+ $this->tableName . '.uid'
+ )
+ )
+ ->where(
+ $query->expr()->eq(
+ 'storage',
+ $this->storage->getUid()
+ ),
+ $query->expr()->eq(
+ 'type',
+ File::FILETYPE_IMAGE
+ ),
+ $query->expr()->eq(
+ 'missing',
+ 0
+ ),
+ $query->expr()->orX(
+ $query->expr()->eq(
+ 'metadata.width',
+ 0
+ ),
+ $query->expr()->eq(
+ 'metadata.height',
+ 0
+ )
+ )
+ );
+
+ return $query->execute()->fetchAllAssociative();
+ }
+}
diff --git a/Classes/Command/CloudinaryFixJpegCommand.php b/Classes/Command/CloudinaryFixJpegCommand.php
index 2b5f1fb..af79fd0 100644
--- a/Classes/Command/CloudinaryFixJpegCommand.php
+++ b/Classes/Command/CloudinaryFixJpegCommand.php
@@ -9,6 +9,7 @@
* LICENSE.md file that was distributed with this source code.
*/
+use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -18,21 +19,13 @@
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
-/**
- * Class CloudinaryFixJpegCommand
- */
class CloudinaryFixJpegCommand extends AbstractCloudinaryCommand
{
- /**
- * @var ResourceStorage
- */
- protected $targetStorage;
-
- /**
- * @param InputInterface $input
- * @param OutputInterface $output
- */
- protected function initialize(InputInterface $input, OutputInterface $output)
+ protected ResourceStorage $targetStorage;
+
+ protected string $tableName = 'sys_file';
+
+ protected function initialize(InputInterface $input, OutputInterface $output): void
{
$this->io = new SymfonyStyle($input, $output);
@@ -43,39 +36,28 @@ protected function initialize(InputInterface $input, OutputInterface $output)
$this->targetStorage = $resourceFactory->getStorageObject($input->getArgument('target'));
}
- /**
- * Configure the command by defining the name, options and arguments
- */
- protected function configure()
+ protected function configure(): void
{
$message = 'After "moving" files you should fix the jpeg extension. Consult README.md for more info.';
$this->setDescription($message)
->addOption('silent', 's', InputOption::VALUE_OPTIONAL, 'Mute output as much as possible', false)
->addOption('yes', 'y', InputOption::VALUE_OPTIONAL, 'Accept everything by default', false)
->addArgument('target', InputArgument::REQUIRED, 'Target storage identifier')
- ->setHelp('Usage: ./vendor/bin/typo3 cloudinary:fix [0-9]');
+ ->setHelp('Usage: ./vendor/bin/typo3 cloudinary:fix:image:after-move [0-9]');
}
- /**
- * Move file
- *
- * @param InputInterface $input
- * @param OutputInterface $output
- *
- * @return int
- */
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (!$this->checkDriverType($this->targetStorage)) {
$this->log('Look out! target storage is not of type "cloudinary"');
- return 1;
+ return Command::INVALID;
}
$files = $this->getJpegFiles();
if (count($files) === 0) {
$this->log('No files found, no work for me!');
- return 0;
+ return Command::SUCCESS;
}
$this->log('I will update %s files by replacing "jpeg" to "jpg" in various fields in storage "%s" (%s)', [
@@ -90,7 +72,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if (!$response) {
$this->log('Script aborted');
- return 0;
+ return Command::SUCCESS;
}
}
@@ -106,20 +88,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$connection->query($query)->execute();
- return 0;
+
+ return Command::SUCCESS;
}
- /**
- * @return array
- */
protected function getJpegFiles(): array
{
- $query = $this->getQueryBuilder();
+ $query = $this->getQueryBuilder($this->tableName);
$query
->select('*')
->from($this->tableName)
->where($query->expr()->eq('storage', $this->targetStorage->getUid()), $query->expr()->eq('extension', $query->expr()->literal('jpeg')));
- return $query->execute()->fetchAll();
+ return $query->execute()->fetchAllAssociative();
}
}
diff --git a/Classes/Command/CloudinaryMetadataCommand.php b/Classes/Command/CloudinaryMetadataCommand.php
new file mode 100644
index 0000000..c29d755
--- /dev/null
+++ b/Classes/Command/CloudinaryMetadataCommand.php
@@ -0,0 +1,122 @@
+io = new SymfonyStyle($input, $output);
+
+ /** @var ResourceFactory $resourceFactory */
+ $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
+ $this->storage = $resourceFactory->getStorageObject($input->getArgument('storage'));
+
+ $this->cloudinaryPathService = GeneralUtility::makeInstance(
+ CloudinaryPathService::class,
+ $this->storage,
+ );
+ }
+
+ protected function configure(): void
+ {
+ $message = 'Set metadata on cloudinary resources such as file reference and file usage.';
+ $this->setDescription($message)
+ ->addOption('silent', 's', InputOption::VALUE_OPTIONAL, 'Mute output as much as possible', false)
+ ->addArgument('storage', InputArgument::REQUIRED, 'Storage identifier')
+ ->setHelp('Usage: ./vendor/bin/typo3 cloudinary:metadata [0-9]');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ if (!$this->checkDriverType($this->storage)) {
+ $this->log('Look out! Storage is not of type "cloudinary"');
+ return Command::INVALID;
+ }
+
+ $q = $this->getQueryBuilder('sys_file');
+ $items = $q->select('file.*', 'reference.*')
+ ->from('sys_file', 'file')
+ ->innerJoin(
+ 'file',
+ 'sys_file_reference',
+ 'reference',
+ 'file.uid = reference.uid_local'
+ )
+ ->where(
+ $q->expr()->eq('file.storage', $this->storage->getUid()),
+ $q->expr()->or(
+ // we could extend to more tables...
+ $q->expr()->eq('tablenames', $q->expr()->literal('tt_content')),
+ $q->expr()->eq('tablenames', $q->expr()->literal('pages'))
+ )
+ )
+ ->execute()
+ ->fetchAllAssociative();
+
+ $site = $this->getFirstSite();
+
+ $publicIdOptions = [];
+ foreach ($items as $item) {
+ $publicId = $this->cloudinaryPathService->computeCloudinaryPublicId($item['identifier']);
+ $publicIdOptions[$publicId]['tags'][$item['pid']] = 't3-page-' . $item['pid'];
+ $publicIdOptions[$publicId]['context']['t3-page-' . $item['pid']] = rtrim((string)$site->getBase(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '?id=' . $item['pid'];
+ }
+
+ // Initialize and configure the API
+ foreach ($publicIdOptions as $publicId => $options) {
+ $this->log('Updating tags and metadata for public id ' . $publicId);
+ $this->getUploadApi()->explicit(
+ $publicId,
+ [
+ 'type' => 'upload',
+ 'tags' => 't3,t3-page,' . implode(', ', $options['tags']),
+ 'context' => $options['context']
+ ]
+ );
+ }
+
+ return Command::SUCCESS;
+ }
+
+ public function getFirstSite(): Site
+ {
+ $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
+ $sites = $siteFinder->getAllSites();
+ return array_values($sites)[0];
+ }
+
+ protected function getUploadApi(): UploadApi
+ {
+ return CloudinaryApiUtility::getCloudinary($this->storage)->uploadApi();
+ }
+
+}
diff --git a/Classes/Command/CloudinaryMoveCommand.php b/Classes/Command/CloudinaryMoveCommand.php
index bea50ea..a5bb353 100644
--- a/Classes/Command/CloudinaryMoveCommand.php
+++ b/Classes/Command/CloudinaryMoveCommand.php
@@ -10,6 +10,7 @@
*/
use Exception;
+use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Style\SymfonyStyle;
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Utility\PathUtility;
@@ -22,40 +23,22 @@
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
-/**
- * Class CloudinaryMoveCommand
- */
class CloudinaryMoveCommand extends AbstractCloudinaryCommand
{
- /**
- * @var array
- */
- protected $faultyUploadedFiles;
+ protected array $faultyUploadedFiles;
- /**
- * @var array
- */
- protected $skippedFiles;
+ protected array $skippedFiles;
- /**
- * @var array
- */
- protected $missingFiles = [];
+ protected array $missingFiles = [];
- /**
- * @var ResourceStorage
- */
- protected $sourceStorage;
+ protected ResourceStorage $sourceStorage;
- /**
- * @var ResourceStorage
- */
- protected $targetStorage;
+ protected ResourceStorage $targetStorage;
/**
* Configure the command by defining the name, options and arguments
*/
- protected function configure()
+ protected function configure(): void
{
$message = 'Move bunch of images to a cloudinary storage. Consult the README.md for more info.';
$this->setDescription($message)
@@ -71,11 +54,7 @@ protected function configure()
->setHelp('Usage: ./vendor/bin/typo3 cloudinary:move 1 2');
}
- /**
- * @param InputInterface $input
- * @param OutputInterface $output
- */
- protected function initialize(InputInterface $input, OutputInterface $output)
+ protected function initialize(InputInterface $input, OutputInterface $output): void
{
$this->io = new SymfonyStyle($input, $output);
@@ -88,26 +67,18 @@ protected function initialize(InputInterface $input, OutputInterface $output)
$this->targetStorage = $resourceFactory->getStorageObject($input->getArgument('target'));
}
- /**
- * Move file
- *
- * @param InputInterface $input
- * @param OutputInterface $output
- *
- * @return int
- */
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (!$this->checkDriverType($this->targetStorage)) {
$this->log('Look out! target storage is not of type "cloudinary"');
- return 1;
+ return Command::INVALID;
}
$files = $this->getFiles($this->sourceStorage, $input);
if (count($files) === 0) {
$this->log('No files found, no work for me!');
- return 0;
+ return Command::SUCCESS;
}
$this->log('I will process %s files to be moved from storage "%s" (%s) to "%s" (%s)', [
@@ -124,7 +95,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if (!$response) {
$this->log('Script aborted');
- return 0;
+
+ return Command::SUCCESS;
}
}
@@ -194,14 +166,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$this->writeLog('skipped', $this->skippedFiles);
}
- return 0;
+ return Command::SUCCESS;
}
- /**
- * @param File $fileObject
- *
- * @return bool
- */
protected function isFileSkipped(File $fileObject): bool
{
$isDisallowedPath = false;
@@ -219,35 +186,23 @@ protected function isFileSkipped(File $fileObject): bool
$isDisallowedPath;
}
- /**
- * @return array
- */
protected function getDisallowedExtensions(): array
{
// Empty for now
return [];
}
- /**
- * @return array
- */
protected function getDisallowedFileIdentifiers(): array
{
// Empty for now
return [];
}
- /**
- * @return array
- */
protected function getDisallowedPaths(): array
{
return ['user_upload/_temp_/', '_temp_/', '_processed_/'];
}
- /**
- * @return object|FileMoveService
- */
protected function getFileMoveService(): FileMoveService
{
return GeneralUtility::makeInstance(FileMoveService::class);
diff --git a/Classes/Command/CloudinaryQueryCommand.php b/Classes/Command/CloudinaryQueryCommand.php
index 964676f..7841cab 100644
--- a/Classes/Command/CloudinaryQueryCommand.php
+++ b/Classes/Command/CloudinaryQueryCommand.php
@@ -9,6 +9,7 @@
* LICENSE.md file that was distributed with this source code.
*/
+use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -21,40 +22,11 @@
use TYPO3\CMS\Core\Utility\GeneralUtility;
use Visol\Cloudinary\Filters\RegularExpressionFilter;
-/**
- * Examples:
- *
- * ./vendor/bin/typo3 cloudinary:query 2
- *
- * # List of files withing a folder
- * ./vendor/bin/typo3 cloudinary:query 2 --path=/foo/
- *
- * # List of files withing a folder with recursive flag
- * ./vendor/bin/typo3 cloudinary:query 2 --path=/foo/ --recursive
- *
- * # List of files withing a folder with filter flag
- * ./vendor/bin/typo3 cloudinary:query 2 --path=/foo/ --filter='[0-9,a-z]\.jpg'
- *
- * # Count files / folder
- * ./vendor/bin/typo3 cloudinary:query 2 --count
- *
- * # List of folders instead of files
- * ./vendor/bin/typo3 cloudinary:query 2 --folder
- *
- * Class CloudinaryQueryCommand
- */
class CloudinaryQueryCommand extends AbstractCloudinaryCommand
{
- /**
- * @var ResourceStorage
- */
- protected $storage;
+ protected ResourceStorage $storage;
- /**
- * @param InputInterface $input
- * @param OutputInterface $output
- */
- protected function initialize(InputInterface $input, OutputInterface $output)
+ protected function initialize(InputInterface $input, OutputInterface $output): void
{
$this->io = new SymfonyStyle($input, $output);
@@ -64,10 +36,31 @@ protected function initialize(InputInterface $input, OutputInterface $output)
$this->storage = $resourceFactory->getStorageObject($input->getArgument('storage'));
}
+ protected string $help = '
+Usage: ./vendor/bin/typo3 cloudinary:query [0-9 - storage id]
+
+Examples
+
+# List of files withing a folder
+typo3 cloudinary:query [0-9] --path=/foo/
+
+# List of files withing a folder with recursive flag
+typo3 cloudinary:query [0-9] --path=/foo/ --recursive
+
+# List of files withing a folder with filter flag
+typo3 cloudinary:query [0-9] --path=/foo/ --filter=\'[0-9,a-z]\.jpg\'
+
+ # Count files / folder
+typo3 cloudinary:query [0-9] --count
+
+ # List of folders instead of files
+typo3 cloudinary:query [0-9] --folder
+ ' ;
+
/**
* Configure the command by defining the name, options and arguments
*/
- protected function configure()
+ protected function configure(): void
{
$message = 'Query a given storage such a list, count files or folders';
$this->setDescription($message)
@@ -79,18 +72,14 @@ protected function configure()
->addOption('recursive', 'r', InputOption::VALUE_NONE, 'Recursive lookup')
->addOption('delete', 'd', InputOption::VALUE_NONE, 'Delete found files / folders.')
->addArgument('storage', InputArgument::REQUIRED, 'Storage identifier')
- ->setHelp('Usage: ./vendor/bin/typo3 cloudinary:query [0-9]');
+ ->setHelp($this->help);
}
- /**
- * @param InputInterface $input
- * @param OutputInterface $output
- */
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (!$this->checkDriverType($this->storage)) {
$this->log('Look out! Storage is not of type "cloudinary"');
- return 1;
+ return Command::INVALID;
}
// Get the chance to define a filter
@@ -141,14 +130,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}
}
- return 0;
+ return Command::SUCCESS;
}
- /**
- * @param InputInterface $input
- *
- * @return array
- */
protected function listFoldersAction(InputInterface $input): array
{
$folders = $this->storage->getFoldersInFolder($this->getFolder($input->getOption('path')), 0, 0, true, $input->getOption('recursive'));
@@ -160,11 +144,6 @@ protected function listFoldersAction(InputInterface $input): array
return $folders;
}
- /**
- * @param InputInterface $input
- *
- * @return array
- */
protected function listFilesAction(InputInterface $input): array
{
$files = $this->storage->getFilesInFolder($this->getFolder($input->getOption('path')), 0, 0, true, $input->getOption('recursive'));
@@ -175,11 +154,6 @@ protected function listFilesAction(InputInterface $input): array
return $files;
}
- /**
- * @param InputInterface $input
- *
- * @return void
- */
protected function countFoldersAction(InputInterface $input): void
{
$numberOfFolders = $this->storage->countFoldersInFolder($this->getFolder($input->getOption('path')), true, $input->getOption('recursive'));
@@ -187,11 +161,6 @@ protected function countFoldersAction(InputInterface $input): void
$this->log('I found %s folder(s)', [$numberOfFolders]);
}
- /**
- * @param InputInterface $input
- *
- * @return void
- */
protected function countFilesAction(InputInterface $input): void
{
$numberOfFiles = $this->storage->countFilesInFolder($this->getFolder($input->getOption('path')), true, $input->getOption('recursive'));
@@ -199,12 +168,7 @@ protected function countFilesAction(InputInterface $input): void
$this->log('I found %s files(s)', [$numberOfFiles]);
}
- /**
- * @param string $folderIdentifier
- *
- * @return object|Folder
- */
- protected function getFolder($folderIdentifier): Folder
+ protected function getFolder(string $folderIdentifier): Folder
{
$folderIdentifier =
$folderIdentifier === DIRECTORY_SEPARATOR ? $folderIdentifier : DIRECTORY_SEPARATOR . trim($folderIdentifier, '/') . DIRECTORY_SEPARATOR;
diff --git a/Classes/Command/CloudinaryScanCommand.php b/Classes/Command/CloudinaryScanCommand.php
index 41abbc1..f11efb2 100644
--- a/Classes/Command/CloudinaryScanCommand.php
+++ b/Classes/Command/CloudinaryScanCommand.php
@@ -9,31 +9,42 @@
* LICENSE.md file that was distributed with this source code.
*/
+use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
+use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use Visol\Cloudinary\Services\CloudinaryScanService;
-/**
- * Class CloudinaryScanCommand
- */
class CloudinaryScanCommand extends AbstractCloudinaryCommand
{
- /**
- * @var ResourceStorage
- */
protected ResourceStorage $storage;
- /**
- * @param InputInterface $input
- * @param OutputInterface $output
- */
- protected function initialize(InputInterface $input, OutputInterface $output)
+ protected string $help = '
+Usage: ./vendor/bin/typo3 cloudinary:scan [0-9]
+
+Examples:
+
+# Query by public id
+typo3 cloudinary:scan
+
+# Query with an additional expression
+typo3 cloudinary:scan --expression="folder=fileadmin/* AND NOT folder:fileadmin/_processed_/*"
+
+Notice:
+
+You can search for an exact folder path with "folder=fileadmin/*"
+or you can search for a folder prefix with "folder:fileadmin/*"
+@see https://cloudinary.com/documentation/search_api
+ ' ;
+
+
+ protected function initialize(InputInterface $input, OutputInterface $output): void
{
$this->io = new SymfonyStyle($input, $output);
@@ -42,48 +53,45 @@ protected function initialize(InputInterface $input, OutputInterface $output)
$this->storage = $resourceFactory->getStorageObject($input->getArgument('storage'));
}
- /**
- * Configure the command by defining the name, options and arguments
- */
- protected function configure()
+ protected function configure(): void
{
$message = 'Scan and warm up a cloudinary storage.';
$this->setDescription($message)
->addOption('silent', 's', InputOption::VALUE_OPTIONAL, 'Mute output as much as possible', false)
->addOption(
- 'empty',
- 'e',
+ 'expression',
+ '',
InputOption::VALUE_OPTIONAL,
- 'Before scanning empty all resources for a given storage',
- false,
+ 'Expression used by the cloudinary search api (e.g --expression="folder=fileadmin/* AND NOT folder=fileadmin/_processed_/*',
+ false
)
->addArgument('storage', InputArgument::REQUIRED, 'Storage identifier')
- ->setHelp('Usage: ./vendor/bin/typo3 cloudinary:scan [0-9]');
+ ->setHelp($this->help);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (!$this->checkDriverType($this->storage)) {
$this->log('Look out! Storage is not of type "cloudinary"');
- return 1;
- }
-
- if ($input->getOption('empty') === null || $input->getOption('empty')) {
- $this->log('Emptying all mirrored resources for storage "%s"', [$this->storage->getUid()]);
- $this->log();
- $this->getCloudinaryScanService()->empty();
+ return Command::INVALID;
}
+ $logFile = Environment::getVarPath() . '/log/cloudinary.log';
$this->log('Hint! Look at the log to get more insight:');
- $this->log('tail -f web/typo3temp/var/logs/cloudinary.log');
+ $this->log('tail -f ' . $logFile);
$this->log();
- $result = $this->getCloudinaryScanService()->scan();
+ /** @var string $expression */
+ $expression = $input->getOption('expression');
+
+ $result = $this->getCloudinaryScanService()
+ ->setAdditionalExpression($expression)
+ ->scan();
$numberOfFiles = $result['created'] + $result['updated'] - $result['deleted'];
if ($numberOfFiles !== $result['total']) {
- $this->error(
- 'Something went wrong. There is a problem with the number of files counted. %s !== %s. It should be fixed in the next scan',
+ $this->warning(
+ 'There is a problem with the number of files counted. %s !== %s. It should be fixed in the next scan',
[$numberOfFiles, $result['total']],
);
}
@@ -99,7 +107,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$result['folder_deleted'],
]);
- return 0;
+ return Command::SUCCESS;
}
protected function getCloudinaryScanService(): CloudinaryScanService
diff --git a/Classes/Command/CloudinaryStorageListCommand.php b/Classes/Command/CloudinaryStorageListCommand.php
new file mode 100644
index 0000000..2ba9b58
--- /dev/null
+++ b/Classes/Command/CloudinaryStorageListCommand.php
@@ -0,0 +1,102 @@
+io = new SymfonyStyle($input, $output);
+
+ }
+
+ protected function configure(): void
+ {
+ $message = 'Display all available storages configured for cloudinary';
+ $this->setDescription($message)
+ ->setHelp($this->help);
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+
+ $cloudinaryRecords = $this->getCloudinaryRecords();
+ foreach ($cloudinaryRecords as $cloudinaryRecord) {
+
+ $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
+ $storage = $resourceFactory->getStorageObject($cloudinaryRecord['uid']);
+
+ $this->log('---');
+ $this->log(sprintf('uid: %s', $storage->getUid()));
+ $this->log(sprintf('name: %s' , $storage->getName()));
+ $configuration = CloudinaryApiUtility::getArrayConfiguration($storage);
+ foreach ($configuration as $key => $value) {
+ if (is_bool($value)) {
+ $value = $value ? 'true' : 'false';
+ }
+ if ($key === 'secure_distribution') {
+ $key .= ' (cname)';
+ }
+ $this->log(sprintf('%s: %s', $key, $value));
+ }
+ }
+
+
+ return Command::SUCCESS;
+ }
+
+ /**
+ * We retrieve all cloudinary storages
+ *
+ * @return array>
+ */
+ protected function getCloudinaryRecords(): array
+ {
+ $q = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file_storage');
+
+ $q->select('*')
+ ->from('sys_file_storage')
+ ->where(
+ $q->expr()->eq('driver', $q->createNamedParameter(CloudinaryDriver::DRIVER_TYPE))
+ );
+
+ return $q->execute()->fetchAllAssociative();
+ }
+
+}
diff --git a/Classes/Controller/CloudinaryAjaxController.php b/Classes/Controller/CloudinaryAjaxController.php
index 104a976..0d94afb 100644
--- a/Classes/Controller/CloudinaryAjaxController.php
+++ b/Classes/Controller/CloudinaryAjaxController.php
@@ -2,23 +2,24 @@
namespace Visol\Cloudinary\Controller;
-use Cloudinary;
+use Cloudinary\Api\Search\SearchApi;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
+use RuntimeException;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use Visol\Cloudinary\Services\CloudinaryPathService;
use Visol\Cloudinary\Services\CloudinaryResourceService;
-use Visol\Cloudinary\Services\ConfigurationService;
+use Visol\Cloudinary\Utility\CloudinaryApiUtility;
class CloudinaryAjaxController
{
public function addFilesAction(ServerRequestInterface $request): ResponseInterface
{
- $storageUid = (int) $request->getParsedBody()['storageUid'];
- $cloudinaryIds = (array) $request->getParsedBody()['cloudinaryIds'];
+ $storageUid = (int)$request->getParsedBody()['storageUid'];
+ $cloudinaryIds = (array)$request->getParsedBody()['cloudinaryIds'];
/** @var ResourceFactory $resourceFactory */
$resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
@@ -29,7 +30,6 @@ public function addFilesAction(ServerRequestInterface $request): ResponseInterfa
try {
// Initialize objects
$storage = $resourceFactory->getStorageObject($storageUid);
- $this->initializeApi($storage);
$cloudinaryPathService = GeneralUtility::makeInstance(
CloudinaryPathService::class,
$storage->getConfiguration(),
@@ -39,13 +39,13 @@ public function addFilesAction(ServerRequestInterface $request): ResponseInterfa
foreach ($cloudinaryIds as $publicId) {
// We must retrieve the resources so that we can determine the format
- $search = new \Cloudinary\Search();
- $search->expression('public_id:' . $publicId);
- $search->max_results(1);
- $response = $search->execute();
+ $response = $this->getSearchApi($storage)
+ ->expression('public_id:' . $publicId)
+ ->maxResults(1)
+ ->execute();
if (empty($response['resources'])) {
- throw new \RuntimeException('Missing resources ' . $publicId, 1657125439);
+ throw new RuntimeException('Missing resources ' . $publicId, 1657125439);
} else {
$resource = $response['resources'][0];
}
@@ -57,13 +57,13 @@ public function addFilesAction(ServerRequestInterface $request): ResponseInterfa
//$cloudinaryPathService->computeFileIdentifier((array) $resource);
// Save mirrored file
- $cloudinaryResourceService->save((array) $resource);
+ $cloudinaryResourceService->save((array)$resource);
// This will trigger a file indexation
$files[] = $storage->getFile($identifier)->getUid();
}
}
- } catch (\RuntimeException $e) {
+ } catch (RuntimeException $e) {
$result = 'ko';
$possibleError = $e->getMessage();
}
@@ -73,16 +73,9 @@ public function addFilesAction(ServerRequestInterface $request): ResponseInterfa
return $response;
}
- protected function initializeApi(ResourceStorage $storage): void
+ protected function getSearchApi(ResourceStorage $storage): SearchApi
{
- $configurationService = GeneralUtility::makeInstance(ConfigurationService::class, $storage->getConfiguration());
-
- Cloudinary::config([
- 'cloud_name' => $configurationService->get('cloudName'),
- 'api_key' => $configurationService->get('apiKey'),
- 'api_secret' => $configurationService->get('apiSecret'),
- 'timeout' => $configurationService->get('timeout'),
- 'secure' => true,
- ]);
+ return CloudinaryApiUtility::getCloudinary($storage)->searchApi();
}
+
}
diff --git a/Classes/Controller/CloudinaryWebHookController.php b/Classes/Controller/CloudinaryWebHookController.php
new file mode 100644
index 0000000..ca759c3
--- /dev/null
+++ b/Classes/Controller/CloudinaryWebHookController.php
@@ -0,0 +1,438 @@
+checkEnvironment();
+
+ /** @var ResourceFactory $resourceFactory */
+ $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
+
+ $storage = $resourceFactory->getStorageObject((int)$this->settings['storage']);
+
+ $this->eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
+
+ $this->cloudinaryResourceService = GeneralUtility::makeInstance(
+ CloudinaryResourceService::class,
+ $storage,
+ );
+
+ $this->scanService = GeneralUtility::makeInstance(
+ CloudinaryScanService::class,
+ $storage
+ );
+
+ $this->cloudinaryPathService = GeneralUtility::makeInstance(
+ CloudinaryPathService::class,
+ $storage
+ );
+
+ $this->storage = $storage;
+
+ $this->processedFileRepository = GeneralUtility::makeInstance(ProcessedFileRepository::class);
+
+ $this->packageManager = GeneralUtility::makeInstance(PackageManager::class);
+ }
+
+ public function processAction(): ResponseInterface
+ {
+ $parsedBody = (string)file_get_contents('php://input');
+ $payload = (array)json_decode($parsedBody, true);
+ self::getLogger()->debug($parsedBody);
+
+ if ($this->shouldStopProcessing($payload)) {
+ return $this->sendResponse(['result' => true, 'message' => 'Nothing to do...']);
+ }
+
+
+ try {
+ [$requestType, $publicIds] = $this->getRequestInfo($payload);
+ $clearCachePages = [];
+
+ self::getLogger()->debug(sprintf('Start flushing cache for file action "%s". ', $requestType));
+
+ foreach ($publicIds as $publicId) {
+ if ($this->isRequestUploadOverwrite($payload)) {
+ // Update caches
+ $this->explicitDataCacheRepository->delete($this->storage->getUid(), $publicId);
+
+ $cloudinaryResource = (array)$this->getAdminApi()->asset($publicId);
+ $this->cloudinaryResourceService->save($cloudinaryResource);
+ }
+ elseif ($requestType === self::NOTIFICATION_TYPE_DELETE) {
+ if (str_contains($publicId, '_processed_')) {
+ $message = 'Processed file deleted. Nothing to do, stopping here...';
+ } else {
+ $message = sprintf('Deleted file "%s", this should not happen. A file is going to be missing.', $publicId);
+ self::getLogger()->warning($message);
+ }
+
+ // early return
+ return $this->sendResponse(['result' => true, 'message' => $message]);
+
+ } elseif ($requestType === self::NOTIFICATION_TYPE_RENAME) { // #. handle file rename
+
+ // Delete the old cache resource
+ $this->cloudinaryResourceService->delete($publicId);
+
+ // Fetch the new cloudinary resource
+ /** @var string $nextPublicId */
+ $nextPublicId = $payload['to_public_id'];
+ $previousCloudinaryResource = $cloudinaryResource = $this->getCloudinaryResource($nextPublicId);
+
+ $previousCloudinaryResource['public_id'] = $publicId;
+ $previousFileIdentifier = $this->cloudinaryPathService->computeFileIdentifier($previousCloudinaryResource);
+ $nextFileIdentifier = $this->cloudinaryPathService->computeFileIdentifier($cloudinaryResource);
+
+ $this->handleFileRename($previousFileIdentifier, $nextFileIdentifier);
+ } else {
+ $cloudinaryResource = $this->getCloudinaryResource($publicId);
+
+ // #. flush cloudinary cdn cache only for valid publicId
+ $this->flushCloudinaryCdn($publicId);
+ }
+
+ // #. retrieve the source file
+ $file = $this->getFile($cloudinaryResource);
+
+ // #. flush the process files
+ $this->clearProcessedFiles($file);
+
+ // #. clean up local temporary file - var/variant folder
+ $this->cleanUpTemporaryFile($file);
+
+ // #. flush cache pages
+ $clearCachePages = $this->clearCachePages($file);
+ }
+ } catch (\Exception $e) {
+ return $this->sendResponse([
+ 'result' => false,
+ 'message' => $e->getMessage(),
+ ]);
+ }
+
+ $message = $clearCachePages
+ ? 'Success! Cache flushed for pages ' . implode(',', $clearCachePages)
+ : 'Success! Job done';
+ return $this->sendResponse(['result' => true, 'message' => $message]);
+ }
+
+ protected function flushCloudinaryCdn(string $publicId): void
+ {
+ // Invalidate CDN cache
+ $this->getUploadApi()->explicit(
+ $publicId,
+ [
+ 'type' => 'upload',
+ 'invalidate' => true
+ ]
+ );
+ }
+
+ protected function handleFileRename(string $previousFileIdentifier, string $nextFileIdentifier): void
+ {
+ $nextFolderIdentifier = PathUtility::dirname($nextFileIdentifier);
+ $nextFolderIdentifierHash = sha1($this->canonicalizeAndCheckFolderIdentifier($nextFolderIdentifier));
+ $nextFileIdentifierHash = sha1($this->canonicalizeAndCheckFileIdentifier($nextFileIdentifier));
+ $nextName = PathUtility::basename($nextFileIdentifier);
+
+ $tableName = 'sys_file';
+ $q = $this->getQueryBuilder($tableName);
+ $q->update($tableName)
+ ->where(
+ $q->expr()->eq('storage', $this->storage->getUid()),
+ $q->expr()->eq('identifier', $q->expr()->literal($previousFileIdentifier))
+ )
+ ->set('identifier', $q->expr()->literal($nextFileIdentifier), false)
+ ->set('identifier_hash', $q->expr()->literal($nextFileIdentifierHash), false)
+ ->set('folder_hash', $q->expr()->literal($nextFolderIdentifierHash), false)
+ ->set('name', $q->expr()->literal($nextName), false)
+ ->setMaxResults(1)
+ ->executeStatement();
+ }
+
+ protected function getFile(array $cloudinaryResource): File
+ {
+ $fileIdentifier = $this->cloudinaryPathService->computeFileIdentifier($cloudinaryResource);
+ $fileIdentifierHash = $this->storage->hashFileIdentifier($fileIdentifier);
+ $tableName = 'sys_file';
+ $q = $this->getQueryBuilder($tableName);
+ $fileRecord = $q->select('*')
+ ->from($tableName)
+ ->where(
+ $q->expr()->eq('storage', $this->storage->getUid()),
+ $q->expr()->eq('identifier_hash', $q->expr()->literal($fileIdentifierHash))
+ )
+ ->execute()
+ ->fetchAssociative();
+
+ if (!$fileRecord) {
+ throw new Exception('No indexed file could be fine for public id ' . $cloudinaryResource['public_id']);
+ }
+
+ $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
+ return $resourceFactory->getFileObject($fileRecord['uid']);
+ }
+
+ protected function getRequestInfo(array $payload): array
+ {
+ if ($this->isRequestUploadOverwrite($payload)) {
+ $requestType = self::NOTIFICATION_TYPE_UPLOAD;
+ $publicIds = [$payload['public_id']];
+ } elseif ($this->isRequestRename($payload)) {
+ $requestType = self::NOTIFICATION_TYPE_RENAME;
+ $publicIds = [$payload['from_public_id']];
+ } elseif ($this->isRequestDelete($payload)) {
+ $requestType = self::NOTIFICATION_TYPE_DELETE;
+ $publicIds = [];
+ foreach ($payload['resources'] as $resource) {
+ $publicIds[] = $resource['public_id'];
+ }
+ } else {
+ throw new UnknownRequestTypeException('Unknown request type', 1677860080);
+ }
+
+ if (empty($publicIds)) {
+ throw new PublicIdMissingException('Missing public id', 1677860090);
+ }
+
+ return [$requestType, $publicIds,];
+ }
+
+ protected function getCloudinaryResource(string $publicId): array
+ {
+ $cloudinaryResource = $this->cloudinaryResourceService->getResource($publicId);
+
+ // The resource does not exist, time to fetch
+ if (!$cloudinaryResource) {
+ $result = $this->scanService->scanOne($publicId);
+ if (!$result) {
+ $message = sprintf('I could not find a corresponding resource for public id %s', $publicId);
+ throw new CloudinaryNotFoundException($message, 1677859470);
+ }
+ $cloudinaryResource = $this->cloudinaryResourceService->getResource($publicId);
+ }
+
+ return $cloudinaryResource;
+ }
+
+ protected function clearProcessedFiles(File $file): void
+ {
+ $processedFiles = $this->processedFileRepository->findAllByOriginalFile($file);
+
+ foreach ($processedFiles as $processedFile) {
+ $processedFile->getStorage()->setEvaluatePermissions(false);
+ $processedFile->delete();
+ }
+ }
+
+ protected function cleanUpTemporaryFile(File $file): void
+ {
+ $temporaryFileNameAndPath = CloudinaryFileUtility::getTemporaryFile($file->getStorage()->getUid(), $file->getIdentifier());
+ if (is_file($temporaryFileNameAndPath)) {
+ self::getLogger()->debug($temporaryFileNameAndPath);
+ unlink($temporaryFileNameAndPath);
+ }
+ }
+
+ protected function clearCachePages(File $file): array
+ {
+ $tags = [];
+ foreach ($this->findPagesWithFileReferences($file) as $page) {
+ $tags[$page['pid']] = 'pageId_' . $page['pid'];
+ }
+
+ GeneralUtility::makeInstance(CacheManager::class)
+ ->flushCachesInGroupByTags('pages', $tags);
+
+ $this->eventDispatcher->dispatch(
+ new ClearCachePageEvent($tags)
+ );
+
+ return array_keys($tags);
+ }
+
+ protected function findPagesWithFileReferences(File $file): array
+ {
+ $queryBuilder = $this->getQueryBuilder('sys_file_reference');
+
+ // @phpstan-ignore-next-line
+ return $queryBuilder
+ ->select('pid')
+ ->from('sys_file_reference')
+ //->groupBy('pid') // no support for distinct
+ ->andWhere(
+ 'pid > 0',
+ 'uid_local = ' . $file->getUid()
+ )
+ ->execute()
+ ->fetchAllAssociative();
+ }
+
+ protected function canonicalizeAndCheckFileIdentifier(string $fileIdentifier): string
+ {
+ return '/' . ltrim($fileIdentifier, '/');
+ }
+
+ protected function canonicalizeAndCheckFolderIdentifier(string $folderPath): string
+ {
+ return rtrim($this->canonicalizeAndCheckFileIdentifier($folderPath), '/') . '/';
+ }
+
+ /**
+ * We only react for notification type "upload", "rename", "delete"
+ * @see other notification types
+ * https://cloudinary.com/documentation/notifications
+ *
+ * - create_folder,
+ * - resource_tags_changed,
+ * - resource_context_changed
+ * - ...
+ */
+ protected function shouldStopProcessing(mixed $payload): bool
+ {
+ return !(
+ $this->isRequestUploadOverwrite($payload) ||
+ $this->isRequestRename($payload) ||
+ $this->isRequestDelete($payload)
+ );
+ }
+
+ protected function isRequestUploadOverwrite(mixed $payload): bool
+ {
+ return is_array($payload) &&
+ array_key_exists('notification_type', $payload) &&
+ array_key_exists('overwritten', $payload) &&
+ $payload['notification_type'] === self::NOTIFICATION_TYPE_UPLOAD
+ && $payload['overwritten'];
+ }
+
+ protected function isRequestRename(mixed $payload): bool
+ {
+ return is_array($payload) &&
+ array_key_exists('notification_type', $payload) &&
+ $payload['notification_type'] === self::NOTIFICATION_TYPE_RENAME;
+ }
+
+ protected function isRequestDelete(mixed $payload): bool
+ {
+ return is_array($payload) &&
+ array_key_exists('notification_type', $payload) &&
+ $payload['notification_type'] === self::NOTIFICATION_TYPE_DELETE;
+ }
+
+ protected function sendResponse(array $data): ResponseInterface
+ {
+ return $this->jsonResponse(
+ (string)json_encode($data)
+ );
+ }
+
+ protected function checkEnvironment(): void
+ {
+ $storageUid = $this->settings['storage'] ?? 0;
+ if ($storageUid <= 0) {
+ throw new RuntimeException('Check your configuration while calling the cloudinary web hook. I am missing a storage id', 1677583654);
+ }
+ }
+
+ protected function getQueryBuilder(string $tableName): QueryBuilder
+ {
+ /** @var ConnectionPool $connectionPool */
+ $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
+ return $connectionPool->getQueryBuilderForTable($tableName);
+ }
+
+ protected static function getLogger(): Logger
+ {
+ /** @var Logger $logger */
+ static $logger = null;
+ // @phpstan-ignore-next-line
+ if ($logger === null) {
+ /** @var LogManager $logger */
+ $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
+ }
+ return $logger;
+ }
+
+ protected function getAdminApi(): AdminApi
+ {
+ return CloudinaryApiUtility::getCloudinary($this->storage)->adminApi();
+ }
+
+ protected function getUploadApi(): UploadApi
+ {
+ return CloudinaryApiUtility::getCloudinary($this->storage)->uploadApi();
+ }
+
+}
diff --git a/Classes/Domain/Repository/ExplicitDataCacheRepository.php b/Classes/Domain/Repository/ExplicitDataCacheRepository.php
index 1a9d68b..93ac5f8 100644
--- a/Classes/Domain/Repository/ExplicitDataCacheRepository.php
+++ b/Classes/Domain/Repository/ExplicitDataCacheRepository.php
@@ -48,7 +48,7 @@ public function findByStorageAndPublicIdAndOptions(int $storageId, string $publi
)
)
);
- $item = $query->execute()->fetch();
+ $item = $query->execute()->fetchAssociative();
if (!$item) {
return null;
diff --git a/Classes/Driver/CloudinaryDriver.php b/Classes/Driver/CloudinaryDriver.php
index 127570c..a7b8787 100644
--- a/Classes/Driver/CloudinaryDriver.php
+++ b/Classes/Driver/CloudinaryDriver.php
@@ -8,116 +8,68 @@
* For the full copyright and license information, please read the
* LICENSE.md file that was distributed with this source code.
*/
+
+use Cloudinary\Api\Admin\AdminApi;
+use Cloudinary\Api\Upload\UploadApi;
+use Exception;
use TYPO3\CMS\Core\Http\ApplicationType;
-use TYPO3\CMS\Core\Charset\CharsetConverter;
-use TYPO3\CMS\Core\Log\Logger;
use TYPO3\CMS\Core\Resource\Exception\InvalidFileNameException;
-use Cloudinary\Api\NotFound;
-use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
-use Cloudinary\Api;
-use Cloudinary\Search;
-use Cloudinary\Uploader;
+use RuntimeException;
+use TYPO3\CMS\Core\Charset\CharsetConverter;
use TYPO3\CMS\Core\Core\Environment;
-use TYPO3\CMS\Core\Type\File\FileInfo;
-use Visol\Cloudinary\Cache\CloudinaryTypo3Cache;
+use TYPO3\CMS\Core\Log\Logger;
+use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Log\LogLevel;
use TYPO3\CMS\Core\Log\LogManager;
-use TYPO3\CMS\Core\Messaging\FlashMessage;
-use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Core\Resource\Driver\AbstractHierarchicalFilesystemDriver;
-use TYPO3\CMS\Core\Resource\Exception;
use TYPO3\CMS\Core\Resource\ResourceStorage;
-use TYPO3\CMS\Extbase\Object\ObjectManager;
-use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
-use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
+use Visol\Cloudinary\Domain\Repository\ExplicitDataCacheRepository;
+use Visol\Cloudinary\Services\CloudinaryFolderService;
+use Visol\Cloudinary\Services\CloudinaryResourceService;
use Visol\Cloudinary\Services\CloudinaryPathService;
+use Visol\Cloudinary\Services\CloudinaryTestConnectionService;
+use Visol\Cloudinary\Services\ConfigurationService;
use Visol\Cloudinary\Utility\CloudinaryApiUtility;
+use Visol\Cloudinary\Utility\CloudinaryFileUtility;
+use Visol\Cloudinary\Utility\MimeTypeUtility;
+use function str_starts_with;
-/**
- * Class CloudinaryDriver
- *
- * @obsolete
- */
class CloudinaryDriver extends AbstractHierarchicalFilesystemDriver
{
public const DRIVER_TYPE = 'VisolCloudinary';
- const ROOT_FOLDER_IDENTIFIER = '/';
- const UNSAFE_FILENAME_CHARACTER_EXPRESSION = '\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF';
-
- /**
- * The base URL that points to this driver's storage. As long is this is not set, it is assumed that this folder
- * is not publicly available
- *
- * @var string
- */
- protected $baseUrl = '';
- /**
- * @var array[]
- */
- protected $cachedCloudinaryResources = [];
+ protected const ROOT_FOLDER_IDENTIFIER = '/';
- /**
- * @var array
- */
- protected $cachedFolders = [];
+ protected const UNSAFE_FILENAME_CHARACTER_EXPRESSION = '\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF';
- /**
- * Object permissions are cached here in subarrays like:
- * $identifier => ['r' => bool, 'w' => bool]
- *
- * @var array
- */
- protected $cachedPermissions = [];
+ public const PROCESSEDFILE_IDENTIFIER_PREFIX = 'PROCESSEDFILE';
- /**
- * Cache to avoid creating multiple local files since it is time consuming.
- * We must download the file.
- *
- * @var array
- */
- protected $localProcessingFiles = [];
+ static public array $knownRawFormats = ['youtube', 'vimeo'];
/**
- * @var ResourceStorage
+ * The base URL that points to this driver's storage. As long is this is not set, it is assumed that this folder
+ * is not publicly available
*/
- protected $storage = null;
+ protected string $baseUrl = '';
/**
- * @var CharsetConverter
+ * Object permissions are cached here in subarrays like:
+ * $identifier => ['r' => bool, 'w' => bool]
*/
- protected $charsetConversion = null;
+ protected array $cachedPermissions = [];
- /**
- * @var string
- */
- protected $languageFile = 'LLL:EXT:cloudinary/Resources/Private/Language/backend.xlf';
+ protected ConfigurationService $configurationService;
- /**
- * @var Dispatcher
- */
- protected $signalSlotDispatcher;
+ protected CharsetConverter $charsetConversion;
- /**
- * @var Api $api
- */
- protected $api;
+ protected ?CloudinaryPathService $cloudinaryPathService = null;
- /**
- * @var CloudinaryTypo3Cache
- */
- protected $cloudinaryTypo3Cache;
+ protected ?CloudinaryResourceService $cloudinaryResourceService = null;
- /**
- * @var CloudinaryPathService
- */
- protected $cloudinaryPathService;
+ protected ?CloudinaryFolderService $cloudinaryFolderService = null;
- /**
- * @param array $configuration
- */
public function __construct(array $configuration = [])
{
$this->configuration = $configuration;
@@ -128,44 +80,56 @@ public function __construct(array $configuration = [])
ResourceStorage::CAPABILITY_BROWSABLE |
ResourceStorage::CAPABILITY_PUBLIC |
ResourceStorage::CAPABILITY_WRITABLE;
+
+ $this->configurationService = GeneralUtility::makeInstance(ConfigurationService::class, $this->configuration);
+
+ $this->charsetConversion = GeneralUtility::makeInstance(CharsetConverter::class);
}
- /**
- * @return void
- */
- public function processConfiguration()
+ public function processConfiguration(): void
{
}
- /**
- * @return void
- */
- public function initialize()
+ public function initialize(): void
{
// Test connection if we are in the edit view of this storage
- if (ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend() && !empty($_GET['edit']['sys_file_storage'])) {
- $this->testConnection();
+ if (
+ !Environment::isCli() &&
+ ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend() &&
+ !empty($_GET['edit']['sys_file_storage'])
+ ) {
+ $this->getCloudinaryTestConnectionService()->test();
}
}
/**
- * @param string $fileIdentifier
- *
- * @return string
+ * @param string $identifier
*/
- public function getPublicUrl($fileIdentifier)
+ public function getPublicUrl($identifier): string
+ {
+ if ($this->isProcessedFile($identifier)) {
+ return $this->getPublicBaseUrl() . $this->getProcessedFileUri($identifier);
+ }
+
+ $cloudinaryResource = $this->getCloudinaryResourceService()->getResource(
+ $this->getCloudinaryPathService()->computeCloudinaryPublicId($identifier),
+ );
+
+ return $cloudinaryResource ? $cloudinaryResource['secure_url'] : '';
+ }
+
+ public function getPublicBaseUrl(): string
{
- return $this->resourceExists($fileIdentifier)
- ? $this->getCachedCloudinaryResource($fileIdentifier)['secure_url']
- : '';
+ $cname = $this->configurationService->get('cname');
+ $publicBaseUrl = empty($cname)
+ ? 'https://res.cloudinary.com/' . $this->configurationService->get('cloudName')
+ : 'https://' . $this->configurationService->get('cname')
+ ;
+
+ return $publicBaseUrl;
}
- /**
- * @param string $message
- * @param array $arguments
- * @param array $data
- */
- protected function log(string $message, array $arguments = [], array $data = [])
+ protected function log(string $message, array $arguments = [], array $data = []): void
{
/** @var Logger $logger */
$logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
@@ -177,88 +141,49 @@ protected function log(string $message, array $arguments = [], array $data = [])
*
* @param string $fileIdentifier
* @param string $hashAlgorithm
- *
- * @return string
*/
- public function hash($fileIdentifier, $hashAlgorithm)
+ public function hash($fileIdentifier, $hashAlgorithm): string
{
return $this->hashIdentifier($fileIdentifier);
}
- /**
- * Returns the identifier of the default folder new files should be put into.
- *
- * @return string
- */
- public function getDefaultFolder()
+ public function getDefaultFolder(): string
{
return $this->getRootLevelFolder();
}
- /**
- * Returns the identifier of the root level folder of the storage.
- *
- * @return string
- */
- public function getRootLevelFolder()
+ public function getRootLevelFolder(): string
{
return DIRECTORY_SEPARATOR;
}
/**
- * Returns information about a file.
- *
* @param string $fileIdentifier
* @param array $propertiesToExtract Array of properties which are be extracted
* If empty all will be extracted
- *
- * @return array
- * @throws \Exception
*/
- public function getFileInfoByIdentifier($fileIdentifier, array $propertiesToExtract = [])
+ public function getFileInfoByIdentifier($fileIdentifier, array $propertiesToExtract = []): array
{
- $this->log(
- 'Just a notice! Time consuming action ahead. I am going to download a file "%s"',
- [$fileIdentifier],
- ['getFileInfoByIdentifier'],
- );
-
- $cloudinaryResource = $this->getCachedCloudinaryResource($fileIdentifier);
-
- // True at the indexation of the file
- // Cloudinary is asynchronous and we might not have the resource at hand.
- // Call it one more time to double check!
-
+ if ($this->isProcessedFile($fileIdentifier)) {
+ return [];
+ }
+ $publicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier);
+ $cloudinaryResource = $this->getCloudinaryResourceService()->getResource($publicId);
+ // We have a problem Hudson!
if (!$cloudinaryResource) {
- $cloudinaryResource = $this->getCloudinaryResource($fileIdentifier);
- $this->flushFileCache(); // We flush the cache....
-
- // This time we have a problem!
- if (!$cloudinaryResource) {
- throw new \Exception(
- 'I could not find a corresponding cloudinary resource for file ' . $fileIdentifier,
- 1591775048,
- );
- }
+ throw new Exception(
+ 'I could not find a corresponding cloudinary resource for file ' . $fileIdentifier,
+ 1591775048,
+ );
}
- // We are force to download the file in order to correctly find the mime type.
- $localFile = $this->getFileForLocalProcessing($fileIdentifier);
-
- /** @var FileInfo $fileInfo */
- $fileInfo = GeneralUtility::makeInstance(FileInfo::class, $localFile);
- $extension = PathUtility::pathinfo($localFile, PATHINFO_EXTENSION);
- $mimeType = $fileInfo->getMimeType();
-
- $canonicalFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier(PathUtility::dirname($fileIdentifier));
-
- $values = [
+ return [
'identifier_hash' => $this->hashIdentifier($fileIdentifier),
- 'folder_hash' => sha1($canonicalFolderIdentifier),
+ 'folder_hash' => sha1($this->canonicalizeAndCheckFolderIdentifier(PathUtility::dirname($fileIdentifier))),
'creation_date' => strtotime($cloudinaryResource['created_at']),
'modification_date' => strtotime($cloudinaryResource['created_at']),
- 'mime_type' => $mimeType,
- 'extension' => $extension,
+ 'mime_type' => MimeTypeUtility::guessMimeType($cloudinaryResource['format']),
+ 'extension' => $this->getResourceInfo($cloudinaryResource, 'format'),
'size' => $this->getResourceInfo($cloudinaryResource, 'bytes'),
'width' => $this->getResourceInfo($cloudinaryResource, 'width'),
'height' => $this->getResourceInfo($cloudinaryResource, 'height'),
@@ -266,89 +191,74 @@ public function getFileInfoByIdentifier($fileIdentifier, array $propertiesToExtr
'identifier' => $fileIdentifier,
'name' => PathUtility::basename($fileIdentifier),
];
-
- return $values;
}
- /**
- * @param array $resource
- * @param string $name
- *
- * @return string
- */
protected function getResourceInfo(array $resource, string $name): string
{
return $resource[$name] ?? '';
}
/**
- * Checks if a file exists
- *
- * @param string $identifier
- *
- * @return bool
+ * @param string $fileIdentifier
*/
- public function fileExists($identifier)
+ public function fileExists($fileIdentifier): bool
{
- if (substr($identifier, -1) === DIRECTORY_SEPARATOR || $identifier === '') {
- return false;
+ // Early return in case we have a processed file.
+ if ($this->isProcessedFile($fileIdentifier)) {
+ return true;
}
- return $this->resourceExists($identifier);
+
+ $cloudinaryResource = $this->getCloudinaryResourceService()->getResource(
+ $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier),
+ );
+
+ return !empty($cloudinaryResource);
}
/**
- * Checks if a folder exists
- *
* @param string $folderIdentifier
- *
- * @return bool
*/
- public function folderExists($folderIdentifier)
+ public function folderExists($folderIdentifier): bool
{
- try {
- // Will trigger an exception if the folder identifier does not exist.
- $subFolders = $this->getFoldersInFolder($folderIdentifier);
- } catch (\Exception $e) {
- return false;
+ // Early return in case we have a processed file.
+ if ($this->isProcessedFolder($folderIdentifier)) {
+ return true;
+ }
+
+ if ($folderIdentifier === self::ROOT_FOLDER_IDENTIFIER) {
+ return true;
}
- return is_array($subFolders);
+ $cloudinaryFolder = $this->getCloudinaryFolderService()->getFolder(
+ $this->getCloudinaryPathService()->computeCloudinaryFolderPath($folderIdentifier),
+ );
+ return !empty($cloudinaryFolder);
}
/**
* @param string $fileName
* @param string $folderIdentifier
- *
- * @return bool
*/
- public function fileExistsInFolder($fileName, $folderIdentifier)
+ public function fileExistsInFolder($fileName, $folderIdentifier): bool
{
- $fileIdentifier = $folderIdentifier . $fileName;
- return $this->resourceExists($fileIdentifier);
+ $fileIdentifier = $this->canonicalizeFolderIdentifierAndFileName($folderIdentifier, $fileName);
+
+ return $this->fileExists($fileIdentifier);
}
/**
- * Checks if a folder exists inside a storage folder
- *
* @param string $folderName
* @param string $folderIdentifier
- *
- * @return bool
*/
- public function folderExistsInFolder($folderName, $folderIdentifier)
+ public function folderExistsInFolder($folderName, $folderIdentifier): bool
{
- $canonicalFolderPath = $this->canonicalizeAndCheckFolderIdentifierAndFolderName($folderIdentifier, $folderName);
- return $this->folderExists($canonicalFolderPath);
+ return $this->folderExists($this->canonicalizeFolderIdentifierAndFolderName($folderIdentifier, $folderName));
}
/**
- * Returns the Identifier for a folder within a given folder.
- *
* @param string $folderName The name of the target folder
* @param string $folderIdentifier
- *
- * @return string
*/
- public function getFolderInFolder($folderName, $folderIdentifier)
+ public function getFolderInFolder($folderName, $folderIdentifier): string
{
return $folderIdentifier . DIRECTORY_SEPARATOR . $folderName;
}
@@ -359,44 +269,39 @@ public function getFolderInFolder($folderName, $folderIdentifier)
* @param string $newFileName optional, if not given original name is used
* @param bool $removeOriginal if set the original file will be removed
* after successful operation
- *
- * @return string the identifier of the new file
- * @throws \Exception
*/
- public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = '', $removeOriginal = true)
+ public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = '', $removeOriginal = true): string
{
$fileName = $this->sanitizeFileName($newFileName !== '' ? $newFileName : PathUtility::basename($localFilePath));
- $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier(
- $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier) . $fileName,
- );
+ $fileIdentifier = $this->canonicalizeFolderIdentifierAndFileName($targetFolderIdentifier, $fileName);
- // Necessary to happen in an early stage.
- $this->log('[CACHE] Flushed as adding file', [], ['addFile']);
- $this->flushFileCache();
+ // We remove a possible existing transient file to avoid bad surprise.
+ $this->cleanUpTemporaryFile($fileIdentifier);
+ // We compute the cloudinary public id
$cloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier);
$this->log(
- '[API][UPLOAD] Cloudinary\Uploader::upload() - add resource "%s"',
+ '[API] UploadApi - add resource "%s"',
[$cloudinaryPublicId],
['addFile()'],
);
- // Before calling API, make sure we are connected with the right "bucket"
- $this->initializeApi();
-
// Upload the file
- $resource = Uploader::upload($localFilePath, [
- 'public_id' => PathUtility::basename($cloudinaryPublicId),
- 'folder' => $this->getCloudinaryPathService()->computeCloudinaryFolderPath($targetFolderIdentifier),
- 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier),
- 'overwrite' => true,
- ]);
+ $cloudinaryResource = (array)$this->getUploadApi()->upload(
+ $localFilePath, [
+ 'public_id' => PathUtility::basename($cloudinaryPublicId),
+ 'folder' => $this->getCloudinaryPathService()->computeCloudinaryFolderPath($targetFolderIdentifier),
+ 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier),
+ 'overwrite' => true,
+ ]
+ );
- if (!$resource && $resource['type'] !== 'upload') {
- throw new \RuntimeException('Cloudinary upload failed for ' . $fileIdentifier, 1591954943);
- }
+ $this->checkCloudinaryUploadStatus($cloudinaryResource, $fileIdentifier);
+
+ // We persist the uploaded resource.
+ $this->getCloudinaryResourceService()->save($cloudinaryResource);
return $fileIdentifier;
}
@@ -405,152 +310,142 @@ public function addFile($localFilePath, $targetFolderIdentifier, $newFileName =
* @param string $fileIdentifier
* @param string $targetFolderIdentifier
* @param string $newFileName
- *
- * @return string
*/
- public function moveFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $newFileName)
+ public function moveFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $newFileName): string
{
$targetIdentifier = $targetFolderIdentifier . $newFileName;
return $this->renameFile($fileIdentifier, $targetIdentifier);
}
/**
- * Copies a file *within* the current storage.
- * Note that this is only about an inner storage copy action,
- * where a file is just copied to another folder in the same storage.
- *
* @param string $fileIdentifier
* @param string $targetFolderIdentifier
* @param string $fileName
- *
- * @return string the Identifier of the new file
*/
- public function copyFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $fileName)
+ public function copyFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $fileName): string
{
- // Flush the file cache entries
- $this->log('[CACHE] Flushed as copying file', [], ['copyFileWithinStorage']);
- $this->flushFileCache();
-
- // Before calling API, make sure we are connected with the right "bucket"
- $this->initializeApi();
+ $targetFileIdentifier = $this->canonicalizeFolderIdentifierAndFileName($targetFolderIdentifier, $fileName);
- Uploader::upload($this->getPublicUrl($fileIdentifier), [
+ $cloudinaryResource = (array)$this->getUploadApi()->upload($this->getPublicUrl($fileIdentifier), [
'public_id' => PathUtility::basename(
- $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileName),
+ $this->getCloudinaryPathService()->computeCloudinaryPublicId($targetFileIdentifier),
),
'folder' => $this->getCloudinaryPathService()->computeCloudinaryFolderPath($targetFolderIdentifier),
'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier),
'overwrite' => true,
]);
- $targetIdentifier = $targetFolderIdentifier . $fileName;
- return $targetIdentifier;
+ $this->checkCloudinaryUploadStatus($cloudinaryResource, $fileIdentifier);
+
+ // We persist the uploaded resource
+ $this->getCloudinaryResourceService()->save($cloudinaryResource);
+
+ return $targetFileIdentifier;
}
/**
- * Replaces a file with file in local file system.
- *
* @param string $fileIdentifier
* @param string $localFilePath
- *
- * @return bool
*/
- public function replaceFile($fileIdentifier, $localFilePath)
+ public function replaceFile($fileIdentifier, $localFilePath): bool
{
+ // We remove a possible existing transient file to avoid bad surprise.
+ $this->cleanUpTemporaryFile($fileIdentifier);
+
$cloudinaryPublicId = PathUtility::basename(
$this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier),
);
- $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath(
- PathUtility::dirname($fileIdentifier),
- );
- $options = [
- 'public_id' => $cloudinaryPublicId,
- 'folder' => $cloudinaryFolder,
+ // Upload the file
+ $cloudinaryResource = (array)$this->getUploadApi()->upload($localFilePath, [
+ 'public_id' => PathUtility::basename($cloudinaryPublicId),
+ 'folder' => $this->getCloudinaryPathService()->computeCloudinaryFolderPath(
+ PathUtility::dirname($fileIdentifier),
+ ),
'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier),
'overwrite' => true,
- ];
+ ]);
- // Flush the file cache entries
- $this->log('[CACHE] Flushed as replacing file', [], ['replaceFile']);
- $this->flushFileCache();
+ $this->checkCloudinaryUploadStatus($cloudinaryResource, $fileIdentifier);
- // Before calling the API, make sure we are connected with the right "bucket"
- $this->initializeApi();
-
- // Upload the file
- Uploader::upload($localFilePath, $options);
+ // We persist the uploaded resource.
+ $this->getCloudinaryResourceService()->save($cloudinaryResource);
return true;
}
/**
- * Removes a file from the filesystem. This does not check if the file is
- * still used or if it is a bad idea to delete it for some other reason
- * this has to be taken care of in the upper layers (e.g. the Storage)!
- *
* @param string $fileIdentifier
- *
- * @return bool TRUE if deleting the file succeeded
*/
- public function deleteFile($fileIdentifier)
+ public function deleteFile($fileIdentifier): bool
{
- // Necessary to happen in an early stage.
- $this->log('[CACHE] Flushed as deleting file', [], ['deleteFile']);
- $this->flushFileCache();
+ if ($this->isProcessedFile($fileIdentifier)) {
+ /*
+ * Processed files do not really exist in the Cloudinary file system
+ * Just remove the cache entry and confirm the deletion.
+ */
+ $this->getCloudinaryResourceService()->delete($fileIdentifier);
+ return true;
+ }
$cloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier);
$this->log(
- '[API][DELETE] Cloudinary\Api::delete_resources - delete resource "%s"',
+ '[API] Delete resource "%s"',
[$cloudinaryPublicId],
['deleteFile'],
);
- $response = $this->getApi()->delete_resources($cloudinaryPublicId, [
+ $response = $this->getAdminApi()->deleteAssets($cloudinaryPublicId, [
'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier),
]);
- $key = is_array($response['deleted']) ? key($response['deleted']) : '';
+ $isDeleted = false;
- return is_array($response['deleted']) &&
- isset($response['deleted'][$key]) &&
- $response['deleted'][$key] === 'deleted';
+ foreach ($response['deleted'] as $publicId => $status) {
+ if ($status === 'deleted') {
+ $isDeleted = (bool)$this->getCloudinaryResourceService()->delete($publicId);
+ }
+ }
+
+ return $isDeleted;
}
/**
- * Removes a folder in filesystem.
- *
* @param string $folderIdentifier
* @param bool $deleteRecursively
- *
- * @return bool
- * @throws Api\GeneralError
*/
- public function deleteFolder($folderIdentifier, $deleteRecursively = false)
+ public function deleteFolder($folderIdentifier, $deleteRecursively = false): bool
{
$cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath($folderIdentifier);
+
if ($deleteRecursively) {
$this->log(
- '[API][DELETE] Cloudinary\Api::delete_resources_by_prefix() - folder "%s"',
+ '[API] Delete folder "%s"',
[$cloudinaryFolder],
['deleteFolder'],
);
- $this->getApi()->delete_resources_by_prefix($cloudinaryFolder);
+ $response = $this->getAdminApi()->deleteAssetsByPrefix($cloudinaryFolder);
+
+ foreach ($response['deleted'] as $publicId => $status) {
+ if ($status === 'deleted') {
+ $this->getCloudinaryResourceService()->delete($publicId);
+ }
+ }
}
// We make sure the folder exists first. It will also delete sub-folder if those ones are empty.
if ($this->folderExists($folderIdentifier)) {
$this->log(
- '[API][DELETE] Cloudinary\Api::delete_folder() - folder "%s"',
+ '[API] Delete folder "%s"',
[$cloudinaryFolder],
['deleteFolder'],
);
- $this->getApi()->delete_folder($cloudinaryFolder);
- }
+ $response = $this->getAdminApi()->deleteFolder($cloudinaryFolder);
- // Flush the folder cache entries
- $this->log('[CACHE][FOLDER] Flushed as deleting folder', [], ['deleteFolder']);
- $this->flushFolderCache();
+ foreach ($response['deleted'] as $folder) {
+ $this->getCloudinaryFolderService()->delete($folder);
+ }
+ }
return true;
}
@@ -558,10 +453,8 @@ public function deleteFolder($folderIdentifier, $deleteRecursively = false)
/**
* @param string $fileIdentifier
* @param bool $writable
- *
- * @return string
*/
- public function getFileForLocalProcessing($fileIdentifier, $writable = true)
+ public function getFileForLocalProcessing($fileIdentifier, $writable = true): string
{
$temporaryPath = $this->getTemporaryPathForFile($fileIdentifier);
@@ -572,73 +465,53 @@ public function getFileForLocalProcessing($fileIdentifier, $writable = true)
['getFileForLocalProcessing'],
);
- $cloudinaryResource = $this->getCloudinaryResource($fileIdentifier);
-
- // We have a problem!
- if (!$cloudinaryResource) {
- throw new \Exception(
- 'I could not find a corresponding cloudinary resource for file ' . $fileIdentifier,
- 1591775049,
- );
- }
-
+ file_put_contents($temporaryPath, file_get_contents($this->getPublicUrl($fileIdentifier)));
$this->log('File downloaded into "%s"', [$temporaryPath], ['getFileForLocalProcessing']);
- file_put_contents($temporaryPath, file_get_contents($cloudinaryResource['secure_url']));
}
return $temporaryPath;
}
/**
- * Creates a new (empty) file and returns the identifier.
- *
* @param string $fileName
* @param string $parentFolderIdentifier
- *
- * @return string
*/
- public function createFile($fileName, $parentFolderIdentifier)
+ public function createFile($fileName, $parentFolderIdentifier): string
{
- throw new \RuntimeException(
+ throw new RuntimeException(
'createFile: not implemented action! Cloudinary Driver is limited to images.',
1570728107,
);
}
/**
- * Creates a folder, within a parent folder.
- * If no parent folder is given, a root level folder will be created
- *
* @param string $newFolderName
* @param string $parentFolderIdentifier
* @param bool $recursive
- *
- * @return string the Identifier of the new folder
*/
- public function createFolder($newFolderName, $parentFolderIdentifier = '', $recursive = false)
+ public function createFolder($newFolderName, $parentFolderIdentifier = '', $recursive = false): string
{
- $canonicalFolderPath = $this->canonicalizeAndCheckFolderIdentifierAndFolderName(
+ $canonicalFolderPath = $this->canonicalizeFolderIdentifierAndFolderName(
$parentFolderIdentifier,
$newFolderName,
);
- $cloudinaryFolder = $this->getCloudinaryPathService()->normalizeCloudinaryPath($canonicalFolderPath);
+ $cloudinaryFolder = $this->getCloudinaryPathService()->normalizeCloudinaryPublicId($canonicalFolderPath);
- $this->log('[API][CREATE] Cloudinary\Api::createFolder() - folder "%s"', [$cloudinaryFolder], ['createFolder']);
- $this->getApi()->create_folder($cloudinaryFolder);
+ $this->log('[API] Create folder "%s"', [$cloudinaryFolder], ['createFolder']);
+ $response = $this->getAdminApi()->createFolder($cloudinaryFolder);
- // Flush the folder cache entries
- $this->log('[CACHE][FOLDER] Flushed as creating folder', [], ['createFolder']);
- $this->flushFolderCache();
+ if (!$response['success']) {
+ throw new Exception('Folder creation failed: ' . $cloudinaryFolder, 1591775050);
+ }
+ $this->getCloudinaryFolderService()->save($cloudinaryFolder);
return $canonicalFolderPath;
}
/**
* @param string $fileIdentifier
- *
- * @return string
*/
- public function getFileContents($fileIdentifier)
+ public function getFileContents($fileIdentifier): string
{
// Will download the file to be faster next time the content is required.
$localFileNameAndPath = $this->getFileForLocalProcessing($fileIdentifier);
@@ -650,23 +523,17 @@ public function getFileContents($fileIdentifier)
*
* @param string $fileIdentifier
* @param string $contents
- *
- * @return int
*/
public function setFileContents($fileIdentifier, $contents)
{
- throw new \RuntimeException('setFileContents: not implemented action!', 1570728106);
+ throw new RuntimeException('setFileContents: not implemented action!', 1570728106);
}
/**
- * Renames a file in this storage.
- *
* @param string $fileIdentifier
* @param string $newFileIdentifier The target path (including the file name!)
- *
- * @return string The identifier of the file after renaming
*/
- public function renameFile($fileIdentifier, $newFileIdentifier)
+ public function renameFile($fileIdentifier, $newFileIdentifier): string
{
if (!$this->isFileIdentifier($newFileIdentifier)) {
$sanitizedFileName = $this->sanitizeFileName(PathUtility::basename($newFileIdentifier));
@@ -680,70 +547,67 @@ public function renameFile($fileIdentifier, $newFileIdentifier)
$newCloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($newFileIdentifier);
if ($cloudinaryPublicId !== $newCloudinaryPublicId) {
- // Necessary to happen in an early stage.
-
- $this->log('[CACHE] Flushed as renaming file', [], ['renameFile']);
- $this->flushFileCache();
-
- // Before calling API, make sure we are connected with the right "bucket"
- $this->initializeApi();
// Rename the file
- Uploader::rename($cloudinaryPublicId, $newCloudinaryPublicId, [
+ $cloudinaryResource = (array)$this->getUploadApi()->rename($cloudinaryPublicId, $newCloudinaryPublicId, [
'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier),
+ 'overwrite' => true,
]);
+
+ $this->checkCloudinaryUploadStatus($cloudinaryResource, $fileIdentifier);
+
+ // We remove the old public id
+ $this->getCloudinaryResourceService()->delete($cloudinaryPublicId);
+
+ // ... and insert the new cloudinary resource
+ $this->getCloudinaryResourceService()->save($cloudinaryResource);
}
return $newFileIdentifier;
}
/**
- * Renames a folder in this storage.
- *
+ * @param array $cloudinaryResource
+ * @param string $fileIdentifier
+ */
+ protected function checkCloudinaryUploadStatus(array $cloudinaryResource, $fileIdentifier): void
+ {
+ if (!$cloudinaryResource && $cloudinaryResource['type'] !== 'upload') {
+ throw new RuntimeException('Cloudinary upload failed for ' . $fileIdentifier, 1591954950);
+ }
+ }
+
+ /**
* @param string $folderIdentifier
* @param string $newFolderName
*
* @return array A map of old to new file identifiers of all affected resources
*/
- public function renameFolder($folderIdentifier, $newFolderName)
+ public function renameFolder($folderIdentifier, $newFolderName): array
{
$renamedFiles = [];
- foreach ($this->getFilesInFolder($folderIdentifier, 0, -1) as $fileIdentifier) {
- $resource = $this->getCachedCloudinaryResource($fileIdentifier);
- $cloudinaryPublicId = $resource['public_id'];
-
- $pathSegments = GeneralUtility::trimExplode('/', $cloudinaryPublicId);
-
- $numberOfSegments = count($pathSegments);
- if ($numberOfSegments > 1) {
- // Replace last folder name by the new folder name
- $pathSegments[$numberOfSegments - 2] = $newFolderName;
- $newCloudinaryPublicId = implode('/', $pathSegments);
-
- if ($cloudinaryPublicId !== $newCloudinaryPublicId) {
- // Flush files + folder cache
- $this->flushCache();
-
- // Before calling the API, make sure we are connected with the right "bucket"
- $this->initializeApi();
-
- // Rename the file
- Uploader::rename($cloudinaryPublicId, $newCloudinaryPublicId, [
- 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier),
- ]);
- $oldFileIdentifier = $this->getCloudinaryPathService()->computeFileIdentifier($resource);
- $newFileIdentifier = $this->getCloudinaryPathService()->computeFileIdentifier([
- 'public_id' => $newCloudinaryPublicId,
- 'format' => $resource['format'],
- ]);
- $renamedFiles[$oldFileIdentifier] = $newFileIdentifier;
+ $pathSegments = GeneralUtility::trimExplode('/', $folderIdentifier);
+ $numberOfSegments = count($pathSegments);
+
+ if ($numberOfSegments > 1) {
+ // Replace last folder name by the new folder name
+ $pathSegments[$numberOfSegments - 2] = $newFolderName;
+ $newFolderIdentifier = implode('/', $pathSegments);
+
+ $renamedFiles[$folderIdentifier] = $newFolderIdentifier;
+
+ foreach ($this->getFilesInFolder($folderIdentifier, 0, -1, true) as $oldFileIdentifier) {
+ $newFileIdentifier = str_replace($folderIdentifier, $newFolderIdentifier, $oldFileIdentifier);
+
+ if ($oldFileIdentifier !== $newFileIdentifier) {
+ $renamedFiles[$oldFileIdentifier] = $this->renameFile($oldFileIdentifier, $newFileIdentifier);
}
}
- }
- // After working so hard, delete the old empty folder.
- $this->deleteFolder($folderIdentifier);
+ // After working so hard, delete the old empty folder.
+ $this->deleteFolder($folderIdentifier);
+ }
return $renamedFiles;
}
@@ -755,10 +619,14 @@ public function renameFolder($folderIdentifier, $newFolderName)
*
* @return array All files which are affected, map of old => new file identifiers
*/
- public function moveFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName)
+ public function moveFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName): array
{
// Compute the new folder identifier and then create it.
- $newTargetFolderIdentifier = $targetFolderIdentifier . $newFolderName . DIRECTORY_SEPARATOR;
+ $newTargetFolderIdentifier = $this->canonicalizeFolderIdentifierAndFolderName(
+ $targetFolderIdentifier,
+ $newFolderName,
+ );
+
if (!$this->folderExists($newTargetFolderIdentifier)) {
$this->createFolder($newTargetFolderIdentifier);
}
@@ -783,13 +651,11 @@ public function moveFolderWithinStorage($sourceFolderIdentifier, $targetFolderId
* @param string $sourceFolderIdentifier
* @param string $targetFolderIdentifier
* @param string $newFolderName
- *
- * @return bool
*/
- public function copyFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName)
+ public function copyFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName): bool
{
// Compute the new folder identifier and then create it.
- $newTargetFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifierAndFolderName(
+ $newTargetFolderIdentifier = $this->canonicalizeFolderIdentifierAndFolderName(
$targetFolderIdentifier,
$newFolderName,
);
@@ -798,11 +664,13 @@ public function copyFolderWithinStorage($sourceFolderIdentifier, $targetFolderId
$this->createFolder($newTargetFolderIdentifier);
}
- $files = $this->getFilesInFolder($sourceFolderIdentifier, 0, -1);
+ $files = $this->getFilesInFolder($sourceFolderIdentifier, 0, -1, true);
foreach ($files as $fileIdentifier) {
+ $newFileIdentifier = str_replace($sourceFolderIdentifier, $newTargetFolderIdentifier, $fileIdentifier);
+
$this->copyFileWithinStorage(
$fileIdentifier,
- $newTargetFolderIdentifier,
+ GeneralUtility::dirname($newFileIdentifier),
PathUtility::basename($fileIdentifier),
);
}
@@ -814,42 +682,17 @@ public function copyFolderWithinStorage($sourceFolderIdentifier, $targetFolderId
* Checks if a folder contains files and (if supported) other folders.
*
* @param string $folderIdentifier
- *
- * @return bool TRUE if there are no files and folders within $folder
*/
- public function isFolderEmpty($folderIdentifier)
+ public function isFolderEmpty($folderIdentifier): bool
{
- $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath($folderIdentifier);
- $this->log(
- '[API] Cloudinary\Api::resources() - fetch files from folder "%s"',
- [$cloudinaryFolder],
- ['isFolderEmpty'],
- );
- $response = $this->getApi()->resources([
- 'resource_type' => 'image',
- 'type' => 'upload',
- 'max_results' => 1,
- 'prefix' => $cloudinaryFolder,
- ]);
-
- return empty($response['resources']);
+ return $this->getCloudinaryFolderService()->countSubFolders($folderIdentifier);
}
/**
- * Checks if a given identifier is within a container, e.g. if
- * a file or folder is within another folder.
- * This can e.g. be used to check for web-mounts.
- *
- * Hint: this also needs to return TRUE if the given identifier
- * matches the container identifier to allow access to the root
- * folder of a filemount.
- *
* @param string $folderIdentifier
* @param string $identifier identifier to be checked against $folderIdentifier
- *
- * @return bool TRUE if $content is within or matches $folderIdentifier
*/
- public function isWithin($folderIdentifier, $identifier)
+ public function isWithin($folderIdentifier, $identifier): bool
{
$folderIdentifier = $this->canonicalizeAndCheckFileIdentifier($folderIdentifier);
$fileIdentifier = $this->canonicalizeAndCheckFileIdentifier($identifier);
@@ -863,219 +706,217 @@ public function isWithin($folderIdentifier, $identifier)
$folderIdentifier .= DIRECTORY_SEPARATOR;
}
- return \str_starts_with($fileIdentifier, $folderIdentifier);
+ return str_starts_with($fileIdentifier, $folderIdentifier);
}
/**
- * Returns information about a file.
- *
* @param string $folderIdentifier
- *
- * @return array
*/
- public function getFolderInfoByIdentifier($folderIdentifier)
+ public function getFolderInfoByIdentifier($folderIdentifier): array
{
$canonicalFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
return [
'identifier' => $canonicalFolderIdentifier,
'name' => PathUtility::basename(
- $this->getCloudinaryPathService()->normalizeCloudinaryPath($canonicalFolderIdentifier),
+ $this->getCloudinaryPathService()->normalizeCloudinaryPublicId($canonicalFolderIdentifier),
),
'storage' => $this->storageUid,
];
}
/**
- * Returns a file inside the specified path
- *
* @param string $fileName
* @param string $folderIdentifier
- *
- * @return string File Identifier
*/
- public function getFileInFolder($fileName, $folderIdentifier)
+ public function getFileInFolder($fileName, $folderIdentifier): string
{
$folderIdentifier = $folderIdentifier . DIRECTORY_SEPARATOR . $fileName;
return $folderIdentifier;
}
/**
- * Returns a list of files inside the specified path
- *
* @param string $folderIdentifier
* @param int $start
* @param int $numberOfItems
* @param bool $recursive
- * @param array $filenameFilterCallbacks callbacks for filtering the items
+ * @param array $filterCallbacks callbacks for filtering the items
* @param string $sort Property name used to sort the items.
* Among them may be: '' (empty, no sorting), name,
* fileext, size, tstamp and rw.
* If a driver does not support the given property, it
* should fall back to "name".
* @param bool $sortRev TRUE to indicate reverse sorting (last to first)
- *
- * @return array of FileIdentifiers
*/
public function getFilesInFolder(
$folderIdentifier,
$start = 0,
$numberOfItems = 40,
$recursive = false,
- array $filenameFilterCallbacks = [],
+ array $filterCallbacks = [],
$sort = '',
$sortRev = false
- ) {
- if ($folderIdentifier === '') {
- throw new \RuntimeException(
- 'Something went wrong in method "getFilesInFolder"! $folderIdentifier can not be empty',
- 1574754623,
- );
- }
+ ): array
+ {
+ $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath(
+ $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier),
+ );
- if (!isset($this->cachedCloudinaryResources[$folderIdentifier])) {
- // Try to fetch from the cache
- $this->cachedCloudinaryResources[$folderIdentifier] = $this->getCache()->getCachedFiles($folderIdentifier);
+ // Set default orderings
+ $parameters = (array)GeneralUtility::_GP('SET');
- // If not found in TYPO3 cache, ask Cloudinary
- if (!is_array($this->cachedCloudinaryResources[$folderIdentifier])) {
- $this->cachedCloudinaryResources[$folderIdentifier] = $this->getCloudinaryResources($folderIdentifier);
- }
+ $orderField = $parameters['sort'] ?? 'filename';
+ if ($orderField === 'file') {
+ $orderField = 'filename';
+ } elseif ($orderField === 'tstamp') {
+ $orderField = 'created_at';
}
- // Set default sorting
- $parameters = (array) GeneralUtility::_GP('SET');
- if (empty($parameters)) {
- $parameters['sort'] = 'file';
- $parameters['reverse'] = 0;
- }
+ $orderings = [
+ 'fieldName' => $orderField,
+ 'direction' => isset($parameters['reverse']) && (int)$parameters['reverse'] ? 'DESC' : 'ASC',
+ ];
- // Sort files
- if ($parameters['sort'] === 'file') {
- if ((int) $parameters['reverse']) {
- uasort(
- $this->cachedCloudinaryResources[$folderIdentifier],
- '\Visol\Cloudinary\Utility\SortingUtility::sortByFileNameDesc',
- );
- } else {
- uasort(
- $this->cachedCloudinaryResources[$folderIdentifier],
- '\Visol\Cloudinary\Utility\SortingUtility::sortByFileNameAsc',
- );
- }
- } elseif ($parameters['sort'] === 'tstamp') {
- if ((int) $parameters['reverse']) {
- uasort(
- $this->cachedCloudinaryResources[$folderIdentifier],
- '\Visol\Cloudinary\Utility\SortingUtility::sortByTimeStampDesc',
- );
- } else {
- uasort(
- $this->cachedCloudinaryResources[$folderIdentifier],
- '\Visol\Cloudinary\Utility\SortingUtility::sortByTimeStampAsc',
- );
- }
- }
+ $pagination = [
+ 'maxResult' => $numberOfItems,
+ 'firstResult' => (int)GeneralUtility::_GP('pointer'),
+ ];
+
+ $cloudinaryResources = $this->getCloudinaryResourceService()->getResources(
+ $cloudinaryFolder,
+ $orderings,
+ $pagination,
+ $recursive,
+ );
- // Pagination
- if ($numberOfItems > 0) {
- $files = array_slice(
- $this->cachedCloudinaryResources[$folderIdentifier],
- (int) GeneralUtility::_GP('pointer'),
- $numberOfItems,
+ // Generate list of folders for the file module.
+ $files = [];
+ foreach ($cloudinaryResources as $cloudinaryResource) {
+ // Compute file identifier
+ $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier(
+ $this->getCloudinaryPathService()->computeFileIdentifier($cloudinaryResource),
);
- } else {
- $files = $this->cachedCloudinaryResources[$folderIdentifier];
+
+ $result = $this->applyFilterMethodsToDirectoryItem(
+ $filterCallbacks,
+ basename($fileIdentifier),
+ $fileIdentifier,
+ dirname($fileIdentifier),
+ );
+
+ if ($result) {
+ $files[] = $fileIdentifier;
+ }
}
- return array_keys($files);
+ return $files;
}
/**
- * Returns the number of files inside the specified path
- *
* @param string $folderIdentifier
* @param bool $recursive
- * @param array $filenameFilterCallbacks callbacks for filtering the items
- *
- * @return int Number of files in folder
+ * @param array $filterCallbacks callbacks for filtering the items
*/
- public function countFilesInFolder($folderIdentifier, $recursive = false, array $filenameFilterCallbacks = [])
+ public function countFilesInFolder($folderIdentifier, $recursive = false, array $filterCallbacks = []): int
{
- if (!isset($this->cachedCloudinaryResources[$folderIdentifier])) {
- $this->getFilesInFolder($folderIdentifier, 0, -1, $recursive, $filenameFilterCallbacks);
+ $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
+
+ // true means we have non-core filters that has been added and we must filter on the PHP side.
+ if (count($filterCallbacks) > 1) {
+ $files = $this->getFilesInFolder($folderIdentifier, 0, 0, $recursive, $filterCallbacks);
+ $result = count($files);
+ } else {
+ $result = $this->getCloudinaryResourceService()->count(
+ $this->getCloudinaryPathService()->computeCloudinaryFolderPath($folderIdentifier),
+ $recursive,
+ );
}
- return count($this->cachedCloudinaryResources[$folderIdentifier]);
+ return $result;
}
/**
- * Returns a list of folders inside the specified path
- *
* @param string $folderIdentifier
* @param int $start
* @param int $numberOfItems
* @param bool $recursive
- * @param array $folderNameFilterCallbacks callbacks for filtering the items
+ * @param array $filterCallbacks
* @param string $sort Property name used to sort the items.
* Among them may be: '' (empty, no sorting), name,
* fileext, size, tstamp and rw.
* If a driver does not support the given property, it
* should fall back to "name".
* @param bool $sortRev TRUE to indicate reverse sorting (last to first)
- *
- * @return array
*/
public function getFoldersInFolder(
$folderIdentifier,
$start = 0,
$numberOfItems = 40,
$recursive = false,
- array $folderNameFilterCallbacks = [],
+ array $filterCallbacks = [],
$sort = '',
$sortRev = false
- ) {
- $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
+ ): array
+ {
+ $parameters = (array)GeneralUtility::_GP('SET');
- if (!isset($this->cachedFolders[$folderIdentifier])) {
- // Try to fetch from the cache
- $this->cachedFolders[$folderIdentifier] = $this->getCache()->getCachedFolders($folderIdentifier);
+ $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath(
+ $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier),
+ );
- // If not found in TYPO3 cache, ask Cloudinary
- if (!is_array($this->cachedFolders[$folderIdentifier])) {
- $this->cachedFolders[$folderIdentifier] = $this->getCloudinaryFolders($folderIdentifier);
- }
- }
+ $cloudinaryFolders = $this->getCloudinaryFolderService()->getSubFolders(
+ $cloudinaryFolder,
+ [
+ 'fieldName' => 'folder',
+ 'direction' => isset($parameters['reverse']) && (int)$parameters['reverse'] ? 'DESC' : 'ASC',
+ ],
+ $recursive,
+ );
+
+ // Generate list of folders for the file module.
+ $folders = [];
+ foreach ($cloudinaryFolders as $cloudinaryFolder) {
+ $folderIdentifier = $this->getCloudinaryPathService()->computeFolderIdentifier($cloudinaryFolder['folder']);
+
+ $result = $this->applyFilterMethodsToDirectoryItem(
+ $filterCallbacks,
+ basename($folderIdentifier),
+ $folderIdentifier,
+ dirname($folderIdentifier),
+ );
- // Sort
- $parameters = (array) GeneralUtility::_GP('SET');
- if (isset($parameters['sort']) && $parameters['sort'] === 'file') {
- (int) $parameters['reverse']
- ? krsort($this->cachedFolders[$folderIdentifier])
- : ksort($this->cachedFolders[$folderIdentifier]);
+ if ($result) {
+ $folders[] = $folderIdentifier;
+ }
}
- return $this->cachedFolders[$folderIdentifier];
+ return $folders;
}
/**
- * Returns the number of folders inside the specified path
- *
* @param string $folderIdentifier
* @param bool $recursive
- * @param array $folderNameFilterCallbacks callbacks for filtering the items
- *
- * @return int Number of folders in folder
+ * @param array $filterCallbacks
*/
- public function countFoldersInFolder($folderIdentifier, $recursive = false, array $folderNameFilterCallbacks = [])
+ public function countFoldersInFolder($folderIdentifier, $recursive = false, array $filterCallbacks = []): int
{
- return count($this->getFoldersInFolder($folderIdentifier, 0, -1, $recursive, $folderNameFilterCallbacks));
+ // true means we have non-core filters that has been added and we must filter on the PHP side.
+ if (count($filterCallbacks) > 1) {
+ $folders = $this->getFoldersInFolder($folderIdentifier, 0, 0, $recursive, $filterCallbacks);
+ $result = count($folders);
+ } else {
+ $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath(
+ $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier),
+ );
+
+ $result = $this->getCloudinaryFolderService()->countSubFolders($cloudinaryFolder, $recursive);
+ }
+
+ return $result;
}
/**
* @param string $identifier
- *
- * @return string
*/
- public function dumpFileContents($identifier)
+ public function dumpFileContents($identifier): string
{
return $this->getFileContents($identifier);
}
@@ -1085,10 +926,8 @@ public function dumpFileContents($identifier)
* (keys r, w) of bool flags
*
* @param string $identifier
- *
- * @return array
*/
- public function getPermissions($identifier)
+ public function getPermissions($identifier): array
{
if (!isset($this->cachedPermissions[$identifier])) {
// Cloudinary does not handle permissions
@@ -1104,10 +943,8 @@ public function getPermissions($identifier)
* and returns the result.
*
* @param int $capabilities
- *
- * @return int
*/
- public function mergeConfigurationCapabilities($capabilities)
+ public function mergeConfigurationCapabilities($capabilities): int
{
$this->capabilities &= $capabilities;
return $this->capabilities;
@@ -1120,13 +957,10 @@ public function mergeConfigurationCapabilities($capabilities)
*
* @param string $fileName Input string, typically the body of a fileName
* @param string $charset Charset of the a fileName (defaults to current charset; depending on context)
- *
- * @return string Output string with any characters not matching [.a-zA-Z0-9_-] is substituted by '_' and trailing dots removed
- * @throws Exception\InvalidFileNameException
*/
- public function sanitizeFileName($fileName, $charset = '')
+ public function sanitizeFileName($fileName, $charset = ''): string
{
- $fileName = $this->getCharsetConversion()->specCharsToASCII('utf-8', $fileName);
+ $fileName = $this->charsetConversion->specCharsToASCII('utf-8', $fileName);
// Replace unwanted characters by underscores
$cleanFileName = preg_replace(
@@ -1141,337 +975,176 @@ public function sanitizeFileName($fileName, $charset = '')
throw new InvalidFileNameException('File name "' . $fileName . '" is invalid.', 1320288991);
}
+ $pathParts = PathUtility::pathinfo($cleanFileName);
+ $fileExtension = $pathParts['extension'] ?? '';
+
+ $cleanFileName =
+ str_replace('.', '_', $pathParts['filename']) .
+ ($fileExtension ? '.' . $fileExtension : '');
+
// Handle the special jpg case which does not correspond to the file extension.
return preg_replace('/jpeg$/', 'jpg', $cleanFileName);
}
/**
- * Returns a temporary path for a given file, including the file extension.
- *
- * @param string $fileIdentifier
+ * Applies a set of filter methods to a file name to find out if it should be used or not. This is e.g. used by
+ * directory listings.
*
- * @return string
+ * @param array $filterMethods The filter methods to use
+ * @param string $itemName
+ * @param string $itemIdentifier
+ * @param string $parentIdentifier
*/
- protected function getTemporaryPathForFile($fileIdentifier): string
+ protected function applyFilterMethodsToDirectoryItem(
+ array $filterMethods,
+ $itemName,
+ $itemIdentifier,
+ $parentIdentifier
+ ): bool
{
- $temporaryFileNameAndPath = sprintf(
- '%s/typo3temp/var/transient/%s%s',
- Environment::getPublicPath(),
- $this->storageUid,
- $fileIdentifier,
- );
-
- $temporaryFolder = GeneralUtility::dirname($temporaryFileNameAndPath);
-
- if (!is_dir($temporaryFolder)) {
- GeneralUtility::mkdir_deep($temporaryFolder);
+ foreach ($filterMethods as $filter) {
+ if (is_callable($filter)) {
+ $result = call_user_func($filter, $itemName, $itemIdentifier, $parentIdentifier, [], $this);
+ // We have to use -1 as the „don't include“ return value, as call_user_func() will return FALSE
+ // If calling the method succeeded and thus we can't use that as a return value.
+ if ($result === -1) {
+ return false;
+ }
+ if ($result === false) {
+ throw new RuntimeException(
+ 'Could not apply file/folder name filter ' . $filter[0] . '::' . $filter[1],
+ 1596795500,
+ );
+ }
+ }
}
- return $temporaryFileNameAndPath;
+ return true;
}
/**
- * @param string $newFileIdentifier
- *
- * @return bool
+ * We want to remove the local temporary file
*/
- protected function isFileIdentifier(string $newFileIdentifier): bool
+ protected function cleanUpTemporaryFile(string $fileIdentifier): void
{
- return false !== strpos($newFileIdentifier, DIRECTORY_SEPARATOR);
- }
+ $temporaryLocalFile = CloudinaryFileUtility::getTemporaryFile($this->storageUid, $fileIdentifier);
+ if (is_file($temporaryLocalFile)) {
+ unlink($temporaryLocalFile);
+ }
- /**
- * @param string $folderIdentifier
- * @param string $folderName
- *
- * @return string
- */
- protected function canonicalizeAndCheckFolderIdentifierAndFolderName(
- string $folderIdentifier,
- string $folderName
- ): string {
- $canonicalFolderPath = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
- return $this->canonicalizeAndCheckFolderIdentifier(
- $canonicalFolderPath . trim($folderName, DIRECTORY_SEPARATOR),
- );
+ // very coupled.... via signal slot?
+ $this->getExplicitDataCacheRepository()->delete($this->storageUid, $fileIdentifier);
}
- /**
- * @param string $folderIdentifier
- *
- * @return array
- * @throws Api\GeneralError
- */
- protected function getCloudinaryFolders(string $folderIdentifier): array
+ public function getExplicitDataCacheRepository(): ExplicitDataCacheRepository
{
- $folders = [];
-
- $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath($folderIdentifier);
-
- $this->log('Fetch subfolders from folder "%s"', [$cloudinaryFolder], ['getCloudinaryFolders']);
-
- $resources = (array) $this->getApi()->subfolders($cloudinaryFolder);
-
- if (!empty($resources['folders'])) {
- foreach ($resources['folders'] as $cloudinaryFolder) {
- $folders[] = $this->canonicalizeAndCheckFolderIdentifierAndFolderName(
- $folderIdentifier,
- $cloudinaryFolder['name'],
- );
- }
- }
-
- // Add result into typo3 cache to spare [API] Calls the next time...
- $this->getCache()->setCachedFolders($folderIdentifier, $folders);
+ return GeneralUtility::makeInstance(ExplicitDataCacheRepository::class);
+ }
- return $folders;
+ protected function isProcessedFile(string $identifier): bool
+ {
+ return str_starts_with($identifier, self::PROCESSEDFILE_IDENTIFIER_PREFIX);
}
- /**
- * @param string $folderIdentifier
- *
- * @return array
- */
- protected function getCloudinaryResources(string $folderIdentifier): array
+ protected function getProcessedFileUri(string $identifier): string
{
- $cloudinaryResources = [];
- $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath($folderIdentifier);
- if (!$cloudinaryFolder) {
- $cloudinaryFolder = self::ROOT_FOLDER_IDENTIFIER . '*';
+ if (! $this->isProcessedFile($identifier)) {
+ throw new \DomainException('Identifier does not belong to a processed file', 1709283844258);
}
- // Before calling the Search API, make sure we are connected with the right cloudinary account
- $this->initializeApi();
-
- do {
- $nextCursor = isset($response) ? $response['next_cursor'] : '';
- $this->log(
- '[API][SEARCH] Cloudinary\Search() - fetch resources from folder "%s" %s',
- [$cloudinaryFolder, $nextCursor ? 'and cursor ' . $nextCursor : ''],
- ['getCloudinaryResources()'],
- );
-
- /** @var Search $search */
- $search = new Search();
- $response = $search
- ->expression('folder=' . $cloudinaryFolder)
- ->sort_by('public_id', 'asc')
- ->max_results(500)
- ->next_cursor($nextCursor)
- ->execute();
-
- if (is_array($response['resources'])) {
- foreach ($response['resources'] as $resource) {
- // Compute file identifier
- $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier(
- $this->getCloudinaryPathService()->computeFileIdentifier($resource),
- );
-
- // Compute folder identifier
- #$computedFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier(
- # GeneralUtility::dirname($fileIdentifier)
- #);
+ return substr($identifier, strlen(self::PROCESSEDFILE_IDENTIFIER_PREFIX));
+ }
- // We manually filter the resources belonging to the given folder to handle the "root" folder case.
- #if ($computedFolderIdentifier === $folderIdentifier) {
- $cloudinaryResources[$fileIdentifier] = $resource;
- #}
- }
- }
- } while (!empty($response) && array_key_exists('next_cursor', $response));
+ protected function isProcessedFolder(string $identifier): bool
+ {
+ $storageRecord = $this->getStorageObject()->getStorageRecord();
- // Add result into typo3 cache to spare API calls next time...
- $this->getCache()->setCachedFiles($folderIdentifier, $cloudinaryResources);
+ // Example value for $storageRecord['processingfolder'] is "2:/_processed_"
+ // we want to remove the "2:" from the expression
+ $processedStorageFolderName = $storageRecord['processingfolder'] ?? '_processed_';
+ $folderPath = preg_replace('/^[0-9]+:/', '', $processedStorageFolderName);
- return $cloudinaryResources;
+ // We detect if the identifier start with the value from $folderPath
+ return str_starts_with($identifier, $folderPath);
}
- /**
- * @param string $fileIdentifier
- *
- * @return array|null
- */
- protected function getCloudinaryResource(string $fileIdentifier)
+ protected function isFileIdentifier(string $newFileIdentifier): bool
{
- $cloudinaryResource = null;
- try {
- // do a double check since we have an asynchronous mechanism.
- $cloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier);
- $resourceType = $this->getCloudinaryPathService()->getResourceType($fileIdentifier);
- $cloudinaryResource = (array) $this->getApi()->resource($cloudinaryPublicId, [
- 'resource_type' => $resourceType,
- ]);
- } catch (NotFound $e) {
- return null;
- }
- return $cloudinaryResource;
+ return str_contains($newFileIdentifier, DIRECTORY_SEPARATOR);
}
- /**
- * @param string $fileIdentifier
- *
- * @return array|false
- */
- protected function getCachedCloudinaryResource(string $fileIdentifier)
+ protected function canonicalizeFolderIdentifierAndFolderName(string $folderIdentifier, string $folderName): string
{
- $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier(GeneralUtility::dirname($fileIdentifier));
-
- // Warm up the cache!
- if (!isset($this->cachedCloudinaryResources[$folderIdentifier][$fileIdentifier])) {
- $this->getFilesInFolder($folderIdentifier, 0, -1);
- }
+ $canonicalFolderPath = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
+ return $this->canonicalizeAndCheckFolderIdentifier(
+ $canonicalFolderPath . trim($folderName, DIRECTORY_SEPARATOR),
+ );
+ }
- return isset($this->cachedCloudinaryResources[$folderIdentifier][$fileIdentifier])
- ? $this->cachedCloudinaryResources[$folderIdentifier][$fileIdentifier]
- : false;
+ protected function canonicalizeFolderIdentifierAndFileName(string $folderIdentifier, string $fileName): string
+ {
+ return $this->canonicalizeAndCheckFileIdentifier(
+ $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier) . $fileName,
+ );
}
- /**
- * @return CloudinaryPathService
- */
- protected function getCloudinaryPathService()
+ protected function getCloudinaryPathService(): CloudinaryPathService
{
if (!$this->cloudinaryPathService) {
$this->cloudinaryPathService = GeneralUtility::makeInstance(
CloudinaryPathService::class,
- $this->configuration,
+ $this->storageUid
+ ? $this->getStorageObject()
+ : $this->configuration,
);
}
return $this->cloudinaryPathService;
}
- /**
- * Test the connection
- */
- protected function testConnection()
+ protected function getStorageObject(): ResourceStorage
{
- $messageQueue = $this->getMessageQueue();
- $localizationPrefix = $this->languageFile . ':driverConfiguration.message.';
- try {
- $this->initializeApi();
-
- $search = new Search();
- $search->expression('folder=' . self::ROOT_FOLDER_IDENTIFIER)->execute();
-
- /** @var FlashMessage $message */
- $message = GeneralUtility::makeInstance(
- FlashMessage::class,
- LocalizationUtility::translate($localizationPrefix . 'connectionTestSuccessful.message'),
- LocalizationUtility::translate($localizationPrefix . 'connectionTestSuccessful.title'),
- FlashMessage::OK,
- );
- $messageQueue->addMessage($message);
- } catch (\Exception $exception) {
- /** @var FlashMessage $message */
- $message = GeneralUtility::makeInstance(
- FlashMessage::class,
- $exception->getMessage(),
- LocalizationUtility::translate($localizationPrefix . 'connectionTestFailed.title'),
- FlashMessage::WARNING,
- );
- $messageQueue->addMessage($message);
- }
+ /** @var ResourceFactory $resourceFactory */
+ $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
+ return $resourceFactory->getStorageObject($this->storageUid);
}
- /**
- * @return FlashMessageQueue
- */
- protected function getMessageQueue()
+ protected function getCloudinaryResourceService(): CloudinaryResourceService
{
- /** @var FlashMessageService $flashMessageService */
- $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
- return $flashMessageService->getMessageQueueByIdentifier();
- }
+ if (!$this->cloudinaryResourceService) {
- /**
- * Checks if an object exists
- *
- * @param string $fileIdentifier
- *
- * @return bool
- */
- protected function resourceExists(string $fileIdentifier)
- {
- // Load from cache
- $cloudinaryResource = $this->getCachedCloudinaryResource($fileIdentifier);
- if (empty($cloudinaryResource)) {
- $cloudinaryResource = $this->getCloudinaryResource($fileIdentifier);
-
- // If we find a cloudinary resource we had a bit of delay.
- // Cloudinary is sometimes asynchronous in the way it handles files.
- // In this case, we better flush the cache...
- if (!empty($cloudinaryResource)) {
- $this->flushFileCache();
- }
- $this->log('Resource with identifier "%s" does not (yet) exist.', [$fileIdentifier], ['resourcesExists()']);
+ $this->cloudinaryResourceService = GeneralUtility::makeInstance(
+ CloudinaryResourceService::class,
+ $this->getStorageObject()
+ );
}
- return !empty($cloudinaryResource);
- }
- /**
- * @return void
- */
- protected function flushCache(): void
- {
- $this->flushFolderCache();
- $this->flushFileCache();
+ return $this->cloudinaryResourceService;
}
- /**
- * @return void
- */
- protected function flushFileCache(): void
+ protected function getCloudinaryTestConnectionService(): CloudinaryTestConnectionService
{
- // Flush the file cache entries
- $this->getCache()->flushFileCache();
-
- $this->cachedCloudinaryResources = [];
+ return GeneralUtility::makeInstance(CloudinaryTestConnectionService::class, $this->configuration);
}
- /**
- * @return void
- */
- protected function flushFolderCache(): void
+ protected function getCloudinaryFolderService(): CloudinaryFolderService
{
- // Flush the file cache entries
- $this->getCache()->flushFolderCache();
-
- $this->cachedFolders = [];
- }
+ if (!$this->cloudinaryFolderService) {
+ $this->cloudinaryFolderService = GeneralUtility::makeInstance(
+ CloudinaryFolderService::class,
+ $this->storageUid,
+ );
+ }
- /**
- * @return void
- */
- protected function initializeApi()
- {
- CloudinaryApiUtility::initializeByConfiguration($this->configuration);
+ return $this->cloudinaryFolderService;
}
- /**
- * @return Api
- */
- protected function getApi()
+ protected function getUploadApi(): UploadApi
{
- $this->initializeApi();
-
- // The object \Cloudinary\Api behaves like a singleton object.
- // The problem: if we have multiple driver instances / configuration, we don't get the expected result
- // meaning we are wrongly fetching resources from other cloudinary "buckets" because of the singleton behaviour
- // Therefore it is better to create a new instance upon each API call to avoid driver confusion
- return new Api();
+ return CloudinaryApiUtility::getCloudinary($this->configuration)->uploadApi();
}
- /**
- * @return CloudinaryTypo3Cache|object
- */
- protected function getCache()
+ protected function getAdminApi(): AdminApi
{
- if ($this->cloudinaryTypo3Cache === null) {
- $this->cloudinaryTypo3Cache = GeneralUtility::makeInstance(
- CloudinaryTypo3Cache::class,
- (int) $this->storageUid,
- );
- }
- return $this->cloudinaryTypo3Cache;
+ return CloudinaryApiUtility::getCloudinary($this->configuration)->adminApi();
}
}
diff --git a/Classes/Driver/CloudinaryFastDriver.php b/Classes/Driver/CloudinaryFastDriver.php
deleted file mode 100644
index 5e5a49c..0000000
--- a/Classes/Driver/CloudinaryFastDriver.php
+++ /dev/null
@@ -1,1357 +0,0 @@
- ['r' => bool, 'w' => bool]
- *
- * @var array
- */
- protected $cachedPermissions = [];
-
- /** @var ConfigurationService */
- protected $configurationService;
-
- /**
- * @var ResourceStorage
- */
- protected $storage = null;
-
- /**
- * @var CharsetConverter
- */
- protected $charsetConversion = null;
-
- /**
- * @var CloudinaryPathService
- */
- protected $cloudinaryPathService;
-
- /**
- * @var CloudinaryResourceService
- */
- protected $cloudinaryResourceService;
-
- /**
- * @var CloudinaryFolderService
- */
- protected $cloudinaryFolderService;
-
- /**
- * @param array $configuration
- */
- public function __construct(array $configuration = [])
- {
- $this->configuration = $configuration;
- parent::__construct($configuration);
-
- // The capabilities default of this driver. See CAPABILITY_* constants for possible values
- $this->capabilities =
- ResourceStorage::CAPABILITY_BROWSABLE |
- ResourceStorage::CAPABILITY_PUBLIC |
- ResourceStorage::CAPABILITY_WRITABLE;
-
- $this->configurationService = GeneralUtility::makeInstance(ConfigurationService::class, $this->configuration);
-
- $this->charsetConversion = GeneralUtility::makeInstance(CharsetConverter::class);
- }
-
- /**
- * @return void
- */
- public function processConfiguration()
- {
- }
-
- /**
- * @return void
- */
- public function initialize()
- {
- // Test connection if we are in the edit view of this storage
- if (
- !Environment::isCli() &&
- ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend() &&
- !empty($_GET['edit']['sys_file_storage'])
- ) {
- $this->getCloudinaryTestConnectionService()->test();
- }
- }
-
- /**
- * @param string $identifier
- *
- * @return string
- */
- public function getPublicUrl($identifier): string
- {
- // for processed file
- $pattern = sprintf('/^PROCESSEDFILE\/(%s\/.*)/', $this->configurationService->get('cloudName'));
- $matches = [];
- if (preg_match($pattern, $identifier, $matches)) {
- return 'https://res.cloudinary.com/' . $matches[1];
- }
-
- $cloudinaryResource = $this->getCloudinaryResourceService()->getResource(
- $this->getCloudinaryPathService()->computeCloudinaryPublicId($identifier),
- );
-
- return $cloudinaryResource ? $cloudinaryResource['secure_url'] : '';
- }
-
- /**
- * @param string $message
- * @param array $arguments
- * @param array $data
- */
- protected function log(string $message, array $arguments = [], array $data = [])
- {
- /** @var Logger $logger */
- $logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
- $logger->log(LogLevel::INFO, vsprintf($message, $arguments), $data);
- }
-
- /**
- * Creates a (cryptographic) hash for a file.
- *
- * @param string $fileIdentifier
- * @param string $hashAlgorithm
- *
- * @return string
- */
- public function hash($fileIdentifier, $hashAlgorithm)
- {
- return $this->hashIdentifier($fileIdentifier);
- }
-
- /**
- * Returns the identifier of the default folder new files should be put into.
- *
- * @return string
- */
- public function getDefaultFolder()
- {
- return $this->getRootLevelFolder();
- }
-
- /**
- * Returns the identifier of the root level folder of the storage.
- *
- * @return string
- */
- public function getRootLevelFolder()
- {
- return DIRECTORY_SEPARATOR;
- }
-
- /**
- * Returns information about a file.
- *
- * @param string $fileIdentifier
- * @param array $propertiesToExtract Array of properties which are be extracted
- * If empty all will be extracted
- *
- * @return array
- * @throws \Exception
- */
- public function getFileInfoByIdentifier($fileIdentifier, array $propertiesToExtract = [])
- {
- $publicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier);
- $cloudinaryResource = $this->getCloudinaryResourceService()->getResource($publicId);
- // We have a problem Hudson!
- if (!$cloudinaryResource) {
- throw new \Exception(
- 'I could not find a corresponding cloudinary resource for file ' . $fileIdentifier,
- 1591775048,
- );
- }
-
- $mimeType = $this->getCloudinaryPathService()->guessMimeType($cloudinaryResource);
- if (!$mimeType) {
- $this->log(
- 'Just a notice! Time consuming action ahead. I am going to download a file "%s"',
- [$fileIdentifier],
- ['getFileInfoByIdentifier'],
- );
-
- // We are force to download the file in order to correctly find the mime type.
- $localFile = $this->getFileForLocalProcessing($fileIdentifier);
-
- /** @var FileInfo $fileInfo */
- $fileInfo = GeneralUtility::makeInstance(FileInfo::class, $localFile);
-
- $mimeType = $fileInfo->getMimeType();
- }
-
- return [
- 'identifier_hash' => $this->hashIdentifier($fileIdentifier),
- 'folder_hash' => sha1($this->canonicalizeAndCheckFolderIdentifier(PathUtility::dirname($fileIdentifier))),
- 'creation_date' => strtotime($cloudinaryResource['created_at']),
- 'modification_date' => strtotime($cloudinaryResource['created_at']),
- 'mime_type' => $mimeType,
- 'extension' => $this->getResourceInfo($cloudinaryResource, 'format'),
- 'size' => $this->getResourceInfo($cloudinaryResource, 'bytes'),
- 'width' => $this->getResourceInfo($cloudinaryResource, 'width'),
- 'height' => $this->getResourceInfo($cloudinaryResource, 'height'),
- 'storage' => $this->storageUid,
- 'identifier' => $fileIdentifier,
- 'name' => PathUtility::basename($fileIdentifier),
- ];
- }
-
- /**
- * @param array $resource
- * @param string $name
- *
- * @return string
- */
- protected function getResourceInfo(array $resource, string $name): string
- {
- return isset($resource[$name]) ? $resource[$name] : '';
- }
-
- /**
- * Checks if a file exists
- *
- * @param string $fileIdentifier
- *
- * @return bool
- */
- public function fileExists($fileIdentifier)
- {
- $cloudinaryResource = $this->getCloudinaryResourceService()->getResource(
- $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier),
- );
-
- return !empty($cloudinaryResource);
- }
-
- /**
- * Checks if a folder exists
- *
- * @param string $folderIdentifier
- *
- * @return bool
- */
- public function folderExists($folderIdentifier)
- {
- if ($folderIdentifier === self::ROOT_FOLDER_IDENTIFIER) {
- return true;
- }
- $cloudinaryFolder = $this->getCloudinaryFolderService()->getFolder(
- $this->getCloudinaryPathService()->computeCloudinaryFolderPath($folderIdentifier),
- );
- return !empty($cloudinaryFolder);
- }
-
- /**
- * @param string $fileName
- * @param string $folderIdentifier
- *
- * @return bool
- */
- public function fileExistsInFolder($fileName, $folderIdentifier)
- {
- $fileIdentifier = $this->canonicalizeFolderIdentifierAndFileName($folderIdentifier, $fileName);
-
- return $this->fileExists($fileIdentifier);
- }
-
- /**
- * Checks if a folder exists inside a storage folder
- *
- * @param string $folderName
- * @param string $folderIdentifier
- *
- * @return bool
- */
- public function folderExistsInFolder($folderName, $folderIdentifier)
- {
- return $this->folderExists($this->canonicalizeFolderIdentifierAndFolderName($folderIdentifier, $folderName));
- }
-
- /**
- * Returns the Identifier for a folder within a given folder.
- *
- * @param string $folderName The name of the target folder
- * @param string $folderIdentifier
- *
- * @return string
- */
- public function getFolderInFolder($folderName, $folderIdentifier)
- {
- return $folderIdentifier . DIRECTORY_SEPARATOR . $folderName;
- }
-
- /**
- * @param string $localFilePath
- * @param string $targetFolderIdentifier
- * @param string $newFileName optional, if not given original name is used
- * @param bool $removeOriginal if set the original file will be removed
- * after successful operation
- *
- * @return string the identifier of the new file
- * @throws \Exception
- */
- public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = '', $removeOriginal = true)
- {
- $fileName = $this->sanitizeFileName($newFileName !== '' ? $newFileName : PathUtility::basename($localFilePath));
-
- $fileIdentifier = $this->canonicalizeFolderIdentifierAndFileName($targetFolderIdentifier, $fileName);
-
- // We remove a possible existing transient file to avoid bad surprise.
- $this->cleanUpTemporaryFile($fileIdentifier);
-
- // We compute the cloudinary public id
- $cloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier);
-
- $this->log(
- '[API][UPLOAD] Cloudinary\Uploader::upload() - add resource "%s"',
- [$cloudinaryPublicId],
- ['addFile()'],
- );
-
- // Before calling API, make sure we are connected with the right "bucket"
- $this->initializeApi();
-
- // Upload the file
- $cloudinaryResource = Uploader::upload($localFilePath, [
- 'public_id' => PathUtility::basename($cloudinaryPublicId),
- 'folder' => $this->getCloudinaryPathService()->computeCloudinaryFolderPath($targetFolderIdentifier),
- 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier),
- 'overwrite' => true,
- ]);
-
- $this->checkCloudinaryUploadStatus($cloudinaryResource, $fileIdentifier);
-
- // We persist the uploaded resource.
- $this->getCloudinaryResourceService()->save($cloudinaryResource);
-
- return $fileIdentifier;
- }
-
- /**
- * @param string $fileIdentifier
- * @param string $targetFolderIdentifier
- * @param string $newFileName
- *
- * @return string
- */
- public function moveFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $newFileName)
- {
- $targetIdentifier = $targetFolderIdentifier . $newFileName;
- return $this->renameFile($fileIdentifier, $targetIdentifier);
- }
-
- /**
- * Copies a file *within* the current storage.
- * Note that this is only about an inner storage copy action,
- * where a file is just copied to another folder in the same storage.
- *
- * @param string $fileIdentifier
- * @param string $targetFolderIdentifier
- * @param string $fileName
- *
- * @return string the Identifier of the new file
- */
- public function copyFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $fileName)
- {
- $targetFileIdentifier = $this->canonicalizeFolderIdentifierAndFileName($targetFolderIdentifier, $fileName);
-
- // Before calling API, make sure we are connected with the right "bucket"
- $this->initializeApi();
-
- $cloudinaryResource = Uploader::upload($this->getPublicUrl($fileIdentifier), [
- 'public_id' => PathUtility::basename(
- $this->getCloudinaryPathService()->computeCloudinaryPublicId($targetFileIdentifier),
- ),
- 'folder' => $this->getCloudinaryPathService()->computeCloudinaryFolderPath($targetFolderIdentifier),
- 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier),
- 'overwrite' => true,
- ]);
-
- $this->checkCloudinaryUploadStatus($cloudinaryResource, $fileIdentifier);
-
- // We persist the uploaded resource
- $this->getCloudinaryResourceService()->save($cloudinaryResource);
-
- return $targetFileIdentifier;
- }
-
- /**
- * Replaces a file with file in local file system.
- *
- * @param string $fileIdentifier
- * @param string $localFilePath
- *
- * @return bool
- */
- public function replaceFile($fileIdentifier, $localFilePath)
- {
- // We remove a possible existing transient file to avoid bad surprise.
- $this->cleanUpTemporaryFile($fileIdentifier);
-
- $cloudinaryPublicId = PathUtility::basename(
- $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier),
- );
-
- // Before calling the API, make sure we are connected with the right "bucket"
- $this->initializeApi();
-
- // Upload the file
- $cloudinaryResource = Uploader::upload($localFilePath, [
- 'public_id' => PathUtility::basename($cloudinaryPublicId),
- 'folder' => $this->getCloudinaryPathService()->computeCloudinaryFolderPath(
- PathUtility::dirname($fileIdentifier),
- ),
- 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier),
- 'overwrite' => true,
- ]);
-
- $this->checkCloudinaryUploadStatus($cloudinaryResource, $fileIdentifier);
-
- // We persist the uploaded resource.
- $this->getCloudinaryResourceService()->save($cloudinaryResource);
-
- return true;
- }
-
- /**
- * Removes a file from the filesystem. This does not check if the file is
- * still used or if it is a bad idea to delete it for some other reason
- * this has to be taken care of in the upper layers (e.g. the Storage)!
- *
- * @param string $fileIdentifier
- *
- * @return bool TRUE if deleting the file succeeded
- */
- public function deleteFile($fileIdentifier)
- {
- $cloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier);
- $this->log(
- '[API][DELETE] Cloudinary\Api::delete_resources - delete resource "%s"',
- [$cloudinaryPublicId],
- ['deleteFile'],
- );
-
- $response = $this->getApi()->delete_resources($cloudinaryPublicId, [
- 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier),
- ]);
-
- $isDeleted = false;
-
- foreach ($response['deleted'] as $publicId => $status) {
- if ($status === 'deleted') {
- $isDeleted = (bool) $this->getCloudinaryResourceService()->delete($publicId);
- }
- }
-
- return $isDeleted;
- }
-
- /**
- * Removes a folder in filesystem.
- *
- * @param string $folderIdentifier
- * @param bool $deleteRecursively
- *
- * @return bool
- * @throws Api\GeneralError
- */
- public function deleteFolder($folderIdentifier, $deleteRecursively = false)
- {
- $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath($folderIdentifier);
-
- if ($deleteRecursively) {
- $this->log(
- '[API][DELETE] Cloudinary\Api::delete_resources_by_prefix() - folder "%s"',
- [$cloudinaryFolder],
- ['deleteFolder'],
- );
- $response = $this->getApi()->delete_resources_by_prefix($cloudinaryFolder);
-
- foreach ($response['deleted'] as $publicId => $status) {
- if ($status === 'deleted') {
- $this->getCloudinaryResourceService()->delete($publicId);
- }
- }
- }
-
- // We make sure the folder exists first. It will also delete sub-folder if those ones are empty.
- if ($this->folderExists($folderIdentifier)) {
- $this->log(
- '[API][DELETE] Cloudinary\Api::delete_folder() - folder "%s"',
- [$cloudinaryFolder],
- ['deleteFolder'],
- );
- $response = $this->getApi()->delete_folder($cloudinaryFolder);
-
- foreach ($response['deleted'] as $folder) {
- $this->getCloudinaryFolderService()->delete($folder);
- }
- }
-
- return true;
- }
-
- /**
- * @param string $fileIdentifier
- * @param bool $writable
- *
- * @return string
- */
- public function getFileForLocalProcessing($fileIdentifier, $writable = true)
- {
- $temporaryPath = $this->getTemporaryPathForFile($fileIdentifier);
-
- if (!is_file($temporaryPath) || !filesize($temporaryPath)) {
- $this->log(
- '[SLOW] Downloading for local processing "%s"',
- [$fileIdentifier],
- ['getFileForLocalProcessing'],
- );
-
- file_put_contents($temporaryPath, file_get_contents($this->getPublicUrl($fileIdentifier)));
- $this->log('File downloaded into "%s"', [$temporaryPath], ['getFileForLocalProcessing']);
- }
-
- return $temporaryPath;
- }
-
- /**
- * Creates a new (empty) file and returns the identifier.
- *
- * @param string $fileName
- * @param string $parentFolderIdentifier
- *
- * @return string
- */
- public function createFile($fileName, $parentFolderIdentifier)
- {
- throw new RuntimeException(
- 'createFile: not implemented action! Cloudinary Driver is limited to images.',
- 1570728107,
- );
- }
-
- /**
- * Creates a folder, within a parent folder.
- * If no parent folder is given, a root level folder will be created
- *
- * @param string $newFolderName
- * @param string $parentFolderIdentifier
- * @param bool $recursive
- *
- * @return string the Identifier of the new folder
- */
- public function createFolder($newFolderName, $parentFolderIdentifier = '', $recursive = false)
- {
- $canonicalFolderPath = $this->canonicalizeFolderIdentifierAndFolderName(
- $parentFolderIdentifier,
- $newFolderName,
- );
- $cloudinaryFolder = $this->getCloudinaryPathService()->normalizeCloudinaryPath($canonicalFolderPath);
-
- $this->log('[API][CREATE] Cloudinary\Api::createFolder() - folder "%s"', [$cloudinaryFolder], ['createFolder']);
- $response = $this->getApi()->create_folder($cloudinaryFolder);
-
- if (!$response['success']) {
- throw new \Exception('Folder creation failed: ' . $cloudinaryFolder, 1591775050);
- }
- $this->getCloudinaryFolderService()->save($cloudinaryFolder);
-
- return $canonicalFolderPath;
- }
-
- /**
- * @param string $fileIdentifier
- *
- * @return string
- */
- public function getFileContents($fileIdentifier)
- {
- // Will download the file to be faster next time the content is required.
- $localFileNameAndPath = $this->getFileForLocalProcessing($fileIdentifier);
- return file_get_contents($localFileNameAndPath);
- }
-
- /**
- * Sets the contents of a file to the specified value.
- *
- * @param string $fileIdentifier
- * @param string $contents
- *
- * @return int
- */
- public function setFileContents($fileIdentifier, $contents)
- {
- throw new RuntimeException('setFileContents: not implemented action!', 1570728106);
- }
-
- /**
- * Renames a file in this storage.
- *
- * @param string $fileIdentifier
- * @param string $newFileIdentifier The target path (including the file name!)
- *
- * @return string The identifier of the file after renaming
- */
- public function renameFile($fileIdentifier, $newFileIdentifier)
- {
- if (!$this->isFileIdentifier($newFileIdentifier)) {
- $sanitizedFileName = $this->sanitizeFileName(PathUtility::basename($newFileIdentifier));
- $folderPath = PathUtility::dirname($fileIdentifier);
- $newFileIdentifier = $this->canonicalizeAndCheckFileIdentifier(
- $this->canonicalizeAndCheckFolderIdentifier($folderPath) . $sanitizedFileName,
- );
- }
-
- $cloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier);
- $newCloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($newFileIdentifier);
-
- if ($cloudinaryPublicId !== $newCloudinaryPublicId) {
- // Before calling API, make sure we are connected with the right "bucket"
- $this->initializeApi();
-
- // Rename the file
- $cloudinaryResource = Uploader::rename($cloudinaryPublicId, $newCloudinaryPublicId, [
- 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier),
- 'overwrite' => true,
- ]);
-
- $this->checkCloudinaryUploadStatus($cloudinaryResource, $fileIdentifier);
-
- // We remove the old public id
- $this->getCloudinaryResourceService()->delete($cloudinaryPublicId);
-
- // ... and insert the new cloudinary resource
- $this->getCloudinaryResourceService()->save($cloudinaryResource);
- }
-
- return $newFileIdentifier;
- }
-
- /**
- * @param array $cloudinaryResource
- * @param string $fileIdentifier
- *
- * @throws Api\GeneralError
- */
- protected function checkCloudinaryUploadStatus(array $cloudinaryResource, $fileIdentifier): void
- {
- if (!$cloudinaryResource && $cloudinaryResource['type'] !== 'upload') {
- throw new RuntimeException('Cloudinary upload failed for ' . $fileIdentifier, 1591954950);
- }
- }
-
- /**
- * Renames a folder in this storage.
- *
- * @param string $folderIdentifier
- * @param string $newFolderName
- *
- * @return array A map of old to new file identifiers of all affected resources
- */
- public function renameFolder($folderIdentifier, $newFolderName)
- {
- $renamedFiles = [];
-
- $pathSegments = GeneralUtility::trimExplode('/', $folderIdentifier);
- $numberOfSegments = count($pathSegments);
-
- if ($numberOfSegments > 1) {
- // Replace last folder name by the new folder name
- $pathSegments[$numberOfSegments - 2] = $newFolderName;
- $newFolderIdentifier = implode('/', $pathSegments);
-
- // Before calling the API, make sure we are connected with the right "bucket"
- $this->initializeApi();
-
- $renamedFiles[$folderIdentifier] = $newFolderIdentifier;
-
- foreach ($this->getFilesInFolder($folderIdentifier, 0, -1, true) as $oldFileIdentifier) {
- $newFileIdentifier = str_replace($folderIdentifier, $newFolderIdentifier, $oldFileIdentifier);
-
- if ($oldFileIdentifier !== $newFileIdentifier) {
- $renamedFiles[$oldFileIdentifier] = $this->renameFile($oldFileIdentifier, $newFileIdentifier);
- }
- }
-
- // After working so hard, delete the old empty folder.
- $this->deleteFolder($folderIdentifier);
- }
-
- return $renamedFiles;
- }
-
- /**
- * @param string $sourceFolderIdentifier
- * @param string $targetFolderIdentifier
- * @param string $newFolderName
- *
- * @return array All files which are affected, map of old => new file identifiers
- */
- public function moveFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName)
- {
- // Compute the new folder identifier and then create it.
- $newTargetFolderIdentifier = $this->canonicalizeFolderIdentifierAndFolderName(
- $targetFolderIdentifier,
- $newFolderName,
- );
-
- if (!$this->folderExists($newTargetFolderIdentifier)) {
- $this->createFolder($newTargetFolderIdentifier);
- }
-
- $movedFiles = [];
- $files = $this->getFilesInFolder($sourceFolderIdentifier, 0, -1);
- foreach ($files as $fileIdentifier) {
- $movedFiles[$fileIdentifier] = $this->moveFileWithinStorage(
- $fileIdentifier,
- $newTargetFolderIdentifier,
- PathUtility::basename($fileIdentifier),
- );
- }
-
- // Delete the old and empty folder
- $this->deleteFolder($sourceFolderIdentifier);
-
- return $movedFiles;
- }
-
- /**
- * @param string $sourceFolderIdentifier
- * @param string $targetFolderIdentifier
- * @param string $newFolderName
- *
- * @return bool
- */
- public function copyFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName)
- {
- // Compute the new folder identifier and then create it.
- $newTargetFolderIdentifier = $this->canonicalizeFolderIdentifierAndFolderName(
- $targetFolderIdentifier,
- $newFolderName,
- );
-
- if (!$this->folderExists($newTargetFolderIdentifier)) {
- $this->createFolder($newTargetFolderIdentifier);
- }
-
- $files = $this->getFilesInFolder($sourceFolderIdentifier, 0, -1, true);
- foreach ($files as $fileIdentifier) {
- $newFileIdentifier = str_replace($sourceFolderIdentifier, $newTargetFolderIdentifier, $fileIdentifier);
-
- $this->copyFileWithinStorage(
- $fileIdentifier,
- GeneralUtility::dirname($newFileIdentifier),
- PathUtility::basename($fileIdentifier),
- );
- }
-
- return true;
- }
-
- /**
- * Checks if a folder contains files and (if supported) other folders.
- *
- * @param string $folderIdentifier
- *
- * @return bool TRUE if there are no files and folders within $folder
- */
- public function isFolderEmpty($folderIdentifier)
- {
- return $this->getCloudinaryFolderService()->countSubFolders($folderIdentifier);
- }
-
- /**
- * Checks if a given identifier is within a container, e.g. if
- * a file or folder is within another folder.
- *
- * Hint: this also needs to return TRUE if the given identifier
- * matches the container identifier to allow access to the root
- * folder of a filemount.
- *
- * @param string $folderIdentifier
- * @param string $identifier identifier to be checked against $folderIdentifier
- *
- * @return bool TRUE if $content is within or matches $folderIdentifier
- */
- public function isWithin($folderIdentifier, $identifier)
- {
- $folderIdentifier = $this->canonicalizeAndCheckFileIdentifier($folderIdentifier);
- $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier($identifier);
- if ($folderIdentifier === $fileIdentifier) {
- return true;
- }
-
- // File identifier canonicalization will not modify a single slash so
- // we must not append another slash in that case.
- if ($folderIdentifier !== DIRECTORY_SEPARATOR) {
- $folderIdentifier .= DIRECTORY_SEPARATOR;
- }
-
- return \str_starts_with($fileIdentifier, $folderIdentifier);
- }
-
- /**
- * Returns information about a file.
- *
- * @param string $folderIdentifier
- *
- * @return array
- */
- public function getFolderInfoByIdentifier($folderIdentifier)
- {
- $canonicalFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
- return [
- 'identifier' => $canonicalFolderIdentifier,
- 'name' => PathUtility::basename(
- $this->getCloudinaryPathService()->normalizeCloudinaryPath($canonicalFolderIdentifier),
- ),
- 'storage' => $this->storageUid,
- ];
- }
-
- /**
- * Returns a file inside the specified path
- *
- * @param string $fileName
- * @param string $folderIdentifier
- *
- * @return string
- */
- public function getFileInFolder($fileName, $folderIdentifier)
- {
- $folderIdentifier = $folderIdentifier . DIRECTORY_SEPARATOR . $fileName;
- return $folderIdentifier;
- }
-
- /**
- * Returns a list of files inside the specified path
- *
- * @param string $folderIdentifier
- * @param int $start
- * @param int $numberOfItems
- * @param bool $recursive
- * @param array $filterCallbacks callbacks for filtering the items
- * @param string $sort Property name used to sort the items.
- * Among them may be: '' (empty, no sorting), name,
- * fileext, size, tstamp and rw.
- * If a driver does not support the given property, it
- * should fall back to "name".
- * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
- *
- * @return array of FileIdentifiers
- */
- public function getFilesInFolder(
- $folderIdentifier,
- $start = 0,
- $numberOfItems = 40,
- $recursive = false,
- array $filterCallbacks = [],
- $sort = '',
- $sortRev = false
- ) {
- $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath(
- $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier),
- );
-
- // Set default orderings
- $parameters = (array) GeneralUtility::_GP('SET');
- if ($parameters['sort'] === 'file') {
- $parameters['sort'] = 'filename';
- } elseif ($parameters['sort'] === 'tstamp') {
- $parameters['sort'] = 'created_at';
- } else {
- $parameters['sort'] = 'filename';
- $parameters['reverse'] = 'ASC';
- }
-
- $orderings = [
- 'fieldName' => $parameters['sort'],
- 'direction' => isset($parameters['reverse']) && (int) $parameters['reverse'] ? 'DESC' : 'ASC',
- ];
-
- $pagination = [
- 'maxResult' => $numberOfItems,
- 'firstResult' => (int) GeneralUtility::_GP('pointer'),
- ];
-
- $cloudinaryResources = $this->getCloudinaryResourceService()->getResources(
- $cloudinaryFolder,
- $orderings,
- $pagination,
- $recursive,
- );
-
- // Generate list of folders for the file module.
- $files = [];
- foreach ($cloudinaryResources as $cloudinaryResource) {
- // Compute file identifier
- $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier(
- $this->getCloudinaryPathService()->computeFileIdentifier($cloudinaryResource),
- );
-
- $result = $this->applyFilterMethodsToDirectoryItem(
- $filterCallbacks,
- basename($fileIdentifier),
- $fileIdentifier,
- dirname($fileIdentifier),
- );
-
- if ($result) {
- $files[] = $fileIdentifier;
- }
- }
-
- return $files;
- }
-
- /**
- * Returns the number of files inside the specified path
- *
- * @param string $folderIdentifier
- * @param bool $recursive
- * @param array $filterCallbacks callbacks for filtering the items
- *
- * @return int
- */
- public function countFilesInFolder($folderIdentifier, $recursive = false, array $filterCallbacks = [])
- {
- $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
-
- // true means we have non-core filters that has been added and we must filter on the PHP side.
- if (count($filterCallbacks) > 1) {
- $files = $this->getFilesInFolder($folderIdentifier, 0, 0, $recursive, $filterCallbacks);
- $result = count($files);
- } else {
- $result = $this->getCloudinaryResourceService()->count(
- $this->getCloudinaryPathService()->computeCloudinaryFolderPath($folderIdentifier),
- $recursive,
- );
- }
- return $result;
- }
-
- /**
- * Returns a list of folders inside the specified path
- *
- * @param string $folderIdentifier
- * @param int $start
- * @param int $numberOfItems
- * @param bool $recursive
- * @param array $filterCallbacks
- * @param string $sort Property name used to sort the items.
- * Among them may be: '' (empty, no sorting), name,
- * fileext, size, tstamp and rw.
- * If a driver does not support the given property, it
- * should fall back to "name".
- * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
- *
- * @return array
- */
- public function getFoldersInFolder(
- $folderIdentifier,
- $start = 0,
- $numberOfItems = 40,
- $recursive = false,
- array $filterCallbacks = [],
- $sort = '',
- $sortRev = false
- ) {
- $parameters = (array) GeneralUtility::_GP('SET');
-
- $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath(
- $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier),
- );
-
- $cloudinaryFolders = $this->getCloudinaryFolderService()->getSubFolders(
- $cloudinaryFolder,
- [
- 'fieldName' => 'folder',
- 'direction' => isset($parameters['reverse']) && (int) $parameters['reverse'] ? 'DESC' : 'ASC',
- ],
- $recursive,
- );
-
- // Generate list of folders for the file module.
- $folders = [];
- foreach ($cloudinaryFolders as $cloudinaryFolder) {
- $folderIdentifier = $this->getCloudinaryPathService()->computeFolderIdentifier($cloudinaryFolder['folder']);
-
- $result = $this->applyFilterMethodsToDirectoryItem(
- $filterCallbacks,
- basename($folderIdentifier),
- $folderIdentifier,
- dirname($folderIdentifier),
- );
-
- if ($result) {
- $folders[] = $folderIdentifier;
- }
- }
-
- return $folders;
- }
-
- /**
- * Returns the number of folders inside the specified path
- *
- * @param string $folderIdentifier
- * @param bool $recursive
- * @param array $filterCallbacks
- *
- * @return int
- */
- public function countFoldersInFolder($folderIdentifier, $recursive = false, array $filterCallbacks = [])
- {
- // true means we have non-core filters that has been added and we must filter on the PHP side.
- if (count($filterCallbacks) > 1) {
- $folders = $this->getFoldersInFolder($folderIdentifier, 0, 0, $recursive, $filterCallbacks);
- $result = count($folders);
- } else {
- $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath(
- $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier),
- );
-
- $result = $this->getCloudinaryFolderService()->countSubFolders($cloudinaryFolder, $recursive);
- }
-
- return $result;
- }
-
- /**
- * @param string $identifier
- *
- * @return string
- */
- public function dumpFileContents($identifier)
- {
- return $this->getFileContents($identifier);
- }
-
- /**
- * Returns the permissions of a file/folder as an array
- * (keys r, w) of bool flags
- *
- * @param string $identifier
- *
- * @return array
- */
- public function getPermissions($identifier)
- {
- if (!isset($this->cachedPermissions[$identifier])) {
- // Cloudinary does not handle permissions
- $permissions = ['r' => true, 'w' => true];
- $this->cachedPermissions[$identifier] = $permissions;
- }
- return $this->cachedPermissions[$identifier];
- }
-
- /**
- * Merges the capabilites merged by the user at the storage
- * configuration into the actual capabilities of the driver
- * and returns the result.
- *
- * @param int $capabilities
- *
- * @return int
- */
- public function mergeConfigurationCapabilities($capabilities)
- {
- $this->capabilities &= $capabilities;
- return $this->capabilities;
- }
-
- /**
- * Returns a string where any character not matching [.a-zA-Z0-9_-] is
- * substituted by '_'
- * Trailing dots are removed
- *
- * @param string $fileName Input string, typically the body of a fileName
- * @param string $charset Charset of the a fileName (defaults to current charset; depending on context)
- *
- * @return string Output string with any characters not matching [.a-zA-Z0-9_-] is substituted by '_' and trailing dots removed
- * @throws Exception\InvalidFileNameException
- */
- public function sanitizeFileName($fileName, $charset = '')
- {
- $fileName = $this->charsetConversion->specCharsToASCII('utf-8', $fileName);
-
- // Replace unwanted characters by underscores
- $cleanFileName = preg_replace(
- '/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . '\\xC0-\\xFF]/',
- '_',
- trim($fileName),
- );
-
- // Strip trailing dots and return
- $cleanFileName = rtrim($cleanFileName, '.');
- if ($cleanFileName === '') {
- throw new InvalidFileNameException('File name "' . $fileName . '" is invalid.', 1320288991);
- }
-
- $pathParts = PathUtility::pathinfo($cleanFileName);
-
- $cleanFileName =
- str_replace('.', '_', $pathParts['filename']) .
- ($pathParts['extension'] ? '.' . $pathParts['extension'] : '');
-
- // Handle the special jpg case which does not correspond to the file extension.
- return preg_replace('/jpeg$/', 'jpg', $cleanFileName);
- }
-
- /**
- * Applies a set of filter methods to a file name to find out if it should be used or not. This is e.g. used by
- * directory listings.
- *
- * @param array $filterMethods The filter methods to use
- * @param string $itemName
- * @param string $itemIdentifier
- * @param string $parentIdentifier
- *
- * @return bool
- * @throws \RuntimeException
- */
- protected function applyFilterMethodsToDirectoryItem(
- array $filterMethods,
- $itemName,
- $itemIdentifier,
- $parentIdentifier
- ) {
- foreach ($filterMethods as $filter) {
- if (is_callable($filter)) {
- $result = call_user_func($filter, $itemName, $itemIdentifier, $parentIdentifier, [], $this);
- // We have to use -1 as the „don't include“ return value, as call_user_func() will return FALSE
- // If calling the method succeeded and thus we can't use that as a return value.
- if ($result === -1) {
- return false;
- }
- if ($result === false) {
- throw new \RuntimeException(
- 'Could not apply file/folder name filter ' . $filter[0] . '::' . $filter[1],
- 1596795500,
- );
- }
- }
- }
- return true;
- }
-
- /**
- * Returns a temporary path for a given file, including the file extension.
- *
- * @param string $fileIdentifier
- *
- * @return string
- */
- protected function getTemporaryPathForFile($fileIdentifier): string
- {
- $temporaryFileNameAndPath =
- Environment::getPublicPath() .
- DIRECTORY_SEPARATOR .
- 'typo3temp/var/transient/' .
- $this->storageUid .
- $fileIdentifier;
-
- $temporaryFolder = GeneralUtility::dirname($temporaryFileNameAndPath);
-
- if (!is_dir($temporaryFolder)) {
- GeneralUtility::mkdir_deep($temporaryFolder);
- }
- return $temporaryFileNameAndPath;
- }
-
- /**
- * We want to remove the local temporary file
- */
- protected function cleanUpTemporaryFile(string $fileIdentifier): void
- {
- $temporaryLocalFile = $this->getTemporaryPathForFile($fileIdentifier);
- if (is_file($temporaryLocalFile)) {
- unlink($temporaryLocalFile);
- }
-
- // very coupled.... via signal slot?
- $this->getExplicitDataCacheRepository()->delete($this->storageUid, $fileIdentifier);
- }
-
- /**
- * @return object|ExplicitDataCacheRepository
- */
- public function getExplicitDataCacheRepository()
- {
- return GeneralUtility::makeInstance(ExplicitDataCacheRepository::class);
- }
-
- /**
- * @param string $newFileIdentifier
- *
- * @return bool
- */
- protected function isFileIdentifier(string $newFileIdentifier): bool
- {
- return false !== strpos($newFileIdentifier, DIRECTORY_SEPARATOR);
- }
-
- /**
- * @param string $folderIdentifier
- * @param string $folderName
- *
- * @return string
- */
- protected function canonicalizeFolderIdentifierAndFolderName(string $folderIdentifier, string $folderName): string
- {
- $canonicalFolderPath = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
- return $this->canonicalizeAndCheckFolderIdentifier(
- $canonicalFolderPath . trim($folderName, DIRECTORY_SEPARATOR),
- );
- }
-
- /**
- * @param string $folderIdentifier
- * @param string $fileName
- *
- * @return string
- */
- protected function canonicalizeFolderIdentifierAndFileName(string $folderIdentifier, string $fileName): string
- {
- return $this->canonicalizeAndCheckFileIdentifier(
- $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier) . $fileName,
- );
- }
-
- /**
- * @return object|CloudinaryPathService
- */
- protected function getCloudinaryPathService()
- {
- if (!$this->cloudinaryPathService) {
- $this->cloudinaryPathService = GeneralUtility::makeInstance(
- CloudinaryPathService::class,
- $this->configuration,
- );
- }
-
- return $this->cloudinaryPathService;
- }
-
- /**
- * @return CloudinaryResourceService
- */
- protected function getCloudinaryResourceService()
- {
- if (!$this->cloudinaryResourceService) {
- /** @var ResourceFactory $resourceFactory */
- $resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
-
- $this->cloudinaryResourceService = GeneralUtility::makeInstance(
- CloudinaryResourceService::class,
- $resourceFactory->getStorageObject($this->storageUid),
- );
- }
-
- return $this->cloudinaryResourceService;
- }
-
- /**
- * @return object|CloudinaryTestConnectionService
- */
- protected function getCloudinaryTestConnectionService()
- {
- return GeneralUtility::makeInstance(CloudinaryTestConnectionService::class, $this->configuration);
- }
-
- /**
- * @return CloudinaryFolderService
- */
- protected function getCloudinaryFolderService()
- {
- if (!$this->cloudinaryFolderService) {
- $this->cloudinaryFolderService = GeneralUtility::makeInstance(
- CloudinaryFolderService::class,
- $this->storageUid,
- );
- }
-
- return $this->cloudinaryFolderService;
- }
-
- /**
- * @return void
- */
- protected function initializeApi()
- {
- Cloudinary::config([
- 'cloud_name' => $this->configurationService->get('cloudName'),
- 'api_key' => $this->configurationService->get('apiKey'),
- 'api_secret' => $this->configurationService->get('apiSecret'),
- 'timeout' => $this->configurationService->get('timeout'),
- 'secure' => true,
- ]);
- }
-
- /**
- * @return Api
- */
- protected function getApi()
- {
- $this->initializeApi();
-
- // The object \Cloudinary\Api behaves like a singleton object.
- // The problem: if we have multiple driver instances / configuration, we don't get the expected result
- // meaning we are wrongly fetching resources from other cloudinary "buckets" because of the singleton behaviour
- // Therefore it is better to create a new instance upon each API call to avoid driver confusion
- return new Api();
- }
-}
diff --git a/Classes/EventHandlers/BeforeFileProcessingEventHandler.php b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php
new file mode 100644
index 0000000..c020369
--- /dev/null
+++ b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php
@@ -0,0 +1,109 @@
+getDriver();
+ if (!$driver instanceof CloudinaryDriver) {
+ return;
+ }
+
+ $processedFile = $event->getProcessedFile();
+ if(! in_array($processedFile->getTaskIdentifier(), self::ALLOWED_TASKS)) {
+ return;
+ }
+ if ($processedFile->isProcessed()) {
+ return;
+ }
+ if (str_starts_with($processedFile->getIdentifier(), 'PROCESSEDFILE')) {
+ return;
+ }
+
+ /** @var File $file */
+ $file = $event->getFile();
+
+ try {
+ $transformations = $this->processingInstructionConverter->convertProcessingConfiguration($event->getProcessedFile()->getTask());
+ } catch (NoCloudinaryTransformation $e) {
+ // Cloudinary is unable to do the required processing
+ return;
+ }
+
+ if ($file->getType() === $file::FILETYPE_VIDEO) {
+ $firstTransformation = $transformations[0];
+ if ($firstTransformation instanceof BaseAction) {
+ $firstTransformation->addQualifiers(new Format('png'));
+ }
+ }
+
+ $transformations[] = [
+ 'fetch_format' => 'auto',
+ 'quality' => 'auto:eco',
+ ];
+
+ $explicitData = $this->getCloudinaryImageService()->getExplicitData($file, [
+ 'type' => 'upload',
+ 'eager' => [$transformations],
+ ]);
+
+ $url = $explicitData['eager'][0]['secure_url'] ?? null;
+ if (!isset($url)) {
+ // cloudinary is unable to render
+ return;
+ }
+
+ $publicBaseUrl = $driver->getPublicBaseUrl();
+ if (! str_starts_with($url, $publicBaseUrl)) {
+ throw new InvalidResourceUrlException($url, $publicBaseUrl, 1709284880259);
+ }
+ $identifier = CloudinaryDriver::PROCESSEDFILE_IDENTIFIER_PREFIX . substr($url, strlen($publicBaseUrl));
+
+ $processedFile->setName(basename($url));
+ $processedFile->setIdentifier($identifier);
+
+ $processedFile->updateProperties([
+ 'width' => $explicitData['eager'][0]['width'],
+ 'height' => $explicitData['eager'][0]['height'],
+ ]);
+
+ $processedFileRepository = GeneralUtility::makeInstance(ProcessedFileRepository::class);
+ $processedFileRepository->add($processedFile);
+ }
+
+ public function getCloudinaryImageService(): CloudinaryImageService
+ {
+ return GeneralUtility::makeInstance(CloudinaryImageService::class);
+ }
+}
diff --git a/Classes/Events/ClearCachePageEvent.php b/Classes/Events/ClearCachePageEvent.php
new file mode 100644
index 0000000..24a347c
--- /dev/null
+++ b/Classes/Events/ClearCachePageEvent.php
@@ -0,0 +1,32 @@
+tags = $tags;
+ }
+
+ public function getTags(): array
+ {
+ return $this->tags;
+ }
+
+ public function setTags(array $tags): ClearCachePageEvent
+ {
+ $this->tags = $tags;
+ return $this;
+ }
+
+}
diff --git a/Classes/Exceptions/CloudinaryNotFoundException.php b/Classes/Exceptions/CloudinaryNotFoundException.php
new file mode 100644
index 0000000..29e640f
--- /dev/null
+++ b/Classes/Exceptions/CloudinaryNotFoundException.php
@@ -0,0 +1,16 @@
+getStorage()->getDriverType() !== CloudinaryFastDriver::DRIVER_TYPE) {
+ if ($file->getStorage()->getDriverType() !== CloudinaryDriver::DRIVER_TYPE) {
return;
}
$cloudinaryImageService = GeneralUtility::makeInstance(CloudinaryImageService::class);
diff --git a/Classes/Services/AbstractCloudinaryMediaService.php b/Classes/Services/AbstractCloudinaryMediaService.php
index fcbf0ae..fb19555 100644
--- a/Classes/Services/AbstractCloudinaryMediaService.php
+++ b/Classes/Services/AbstractCloudinaryMediaService.php
@@ -2,7 +2,7 @@
namespace Visol\Cloudinary\Services;
-use Cloudinary\Uploader;
+use Cloudinary\Api\Upload\UploadApi;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use TYPO3\CMS\Core\Log\Logger;
use TYPO3\CMS\Core\Log\LogLevel;
@@ -10,35 +10,11 @@
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
-use Visol\Cloudinary\Driver\CloudinaryDriver;
use Visol\Cloudinary\Utility\CloudinaryApiUtility;
abstract class AbstractCloudinaryMediaService
{
- /**
- * @throws \Exception
- */
- protected function initializeApi(ResourceStorage $storage): void
- {
- // Check the file is stored on the right storage
- // If not we should trigger an exception
- if ($storage->getDriverType() !== CloudinaryDriver::DRIVER_TYPE) {
- $message = sprintf(
- 'Wrong storage! Can not initialize with storage type "%s".',
- $storage->getDriverType()
- );
- throw new \Exception($message, 1590401459);
- }
-
- CloudinaryApiUtility::initializeByConfiguration($storage->getConfiguration());
- }
- /**
- * @param File $file
- * @param array $options
- *
- * @return array
- */
public function getExplicitData(File $file, array $options): array
{
$publicId = $this->getPublicIdForFile($file);
@@ -46,8 +22,7 @@ public function getExplicitData(File $file, array $options): array
$explicitData = $this->explicitDataCacheRepository->findByStorageAndPublicIdAndOptions($file->getStorage()->getUid(), $publicId, $options)['explicit_data'];
if (!$explicitData) {
- $this->initializeApi($file->getStorage());
- $explicitData = Uploader::explicit($publicId, $options);
+ $explicitData = $this->getUploadApi($file->getStorage())->explicit($publicId, $options);
try {
$this->explicitDataCacheRepository->save($file->getStorage()->getUid(), $publicId, $options, $explicitData);
} catch (UniqueConstraintViolationException $e) {
@@ -58,12 +33,7 @@ public function getExplicitData(File $file, array $options): array
return $explicitData;
}
- /**
- * @param string $message
- * @param array $arguments
- * @param array $data
- */
- protected function error(string $message, array $arguments = [], array $data = [])
+ protected function error(string $message, array $arguments = [], array $data = []): void
{
/** @var Logger $logger */
$logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
@@ -74,32 +44,21 @@ protected function error(string $message, array $arguments = [], array $data = [
);
}
- /**
- * @return File
- */
public function getEmergencyPlaceholderFile(): File
{
/** @var CloudinaryUploadService $cloudinaryUploadService */
$cloudinaryUploadService = GeneralUtility::makeInstance(CloudinaryUploadService::class);
- return $cloudinaryUploadService->uploadLocalFile('');
+ return $cloudinaryUploadService->getEmergencyFile();
}
- /**
- * @return object|CloudinaryPathService
- */
- protected function getCloudinaryPathService(ResourceStorage $storage)
+ protected function getCloudinaryPathService(ResourceStorage $storage): CloudinaryPathService
{
return GeneralUtility::makeInstance(
CloudinaryPathService::class,
- $storage->getConfiguration()
+ $storage
);
}
- /**
- * @param File $file
- *
- * @return string
- */
public function getPublicIdForFile(File $file): string
{
@@ -113,9 +72,19 @@ public function getPublicIdForFile(File $file): string
}
// Compute the cloudinary public id
- $publicId = $this
+ return $this
->getCloudinaryPathService($file->getStorage())
->computeCloudinaryPublicId($file->getIdentifier());
- return $publicId;
}
+
+ public function getResourceTypeForFile(File $file): string
+ {
+ return $this->getCloudinaryPathService($file->getStorage())->getResourceType($file->getIdentifier());
+ }
+
+ protected function getUploadApi(ResourceStorage $storage): UploadApi
+ {
+ return CloudinaryApiUtility::getCloudinary($storage)->uploadApi();
+ }
+
}
diff --git a/Classes/Services/CloudinaryFolderService.php b/Classes/Services/CloudinaryFolderService.php
index 1102c01..1bd1954 100644
--- a/Classes/Services/CloudinaryFolderService.php
+++ b/Classes/Services/CloudinaryFolderService.php
@@ -14,37 +14,18 @@
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
-/**
- * Class CloudinaryFolderService
- */
class CloudinaryFolderService
{
- /**
- * @var string
- */
- protected $tableName = 'tx_cloudinary_folder';
+ protected string $tableName = 'tx_cloudinary_folder';
- /**
- * @var int
- */
- protected $storageUid;
+ protected int $storageUid;
- /**
- * CloudinaryResourceService constructor.
- *
- * @param int $storageUid
- */
public function __construct(int $storageUid)
{
$this->storageUid = $storageUid;
}
- /**
- * @param string $folder
- *
- * @return array
- */
public function getFolder(string $folder): array
{
$query = $this->getQueryBuilder();
@@ -59,15 +40,10 @@ public function getFolder(string $folder): array
)
);
- $folder = $query->execute()->fetch();
- return $folder
- ? $folder
- : [];
+ $folder = $query->execute()->fetchAssociative();
+ return $folder ?: [];
}
- /**
- * @return int
- */
public function markAsMissing(): int
{
$values = ['missing' => 1,];
@@ -75,12 +51,6 @@ public function markAsMissing(): int
return $this->getConnection()->update($this->tableName, $values, $identifier);
}
- /**
- * @param string $parentFolder
- * @param array $orderings
- *
- * @return array
- */
public function getSubFolders(string $parentFolder, array $orderings, bool $recursive = false): array
{
$query = $this->getQueryBuilder();
@@ -104,15 +74,9 @@ public function getSubFolders(string $parentFolder, array $orderings, bool $recu
);
$query->andWhere($expresion);
- return $query->execute()->fetchAll();
+ return $query->execute()->fetchAllAssociative();
}
- /**
- * @param string $parentFolder
- * @param bool $recursive
- *
- * @return int
- */
public function countSubFolders(string $parentFolder, bool $recursive = false): int
{
$query = $this->getQueryBuilder();
@@ -135,14 +99,9 @@ public function countSubFolders(string $parentFolder, bool $recursive = false):
);
$query->andWhere($expresion);
- return (int)$query->execute()->fetchColumn(0);
+ return (int)$query->execute()->fetchOne(0);
}
- /**
- * @param string $folder
- *
- * @return int
- */
public function delete(string $folder): int
{
$identifier['folder'] = $folder;
@@ -150,22 +109,12 @@ public function delete(string $folder): int
return $this->getConnection()->delete($this->tableName, $identifier);
}
- /**
- * @param array $identifier
- *
- * @return int
- */
- public function deleteAll(array $identifier = []): int
+ public function deleteAll(array $identifiers = []): int
{
- $identifier['storage'] = $this->storageUid;
- return $this->getConnection()->delete($this->tableName, $identifier);
+ $identifiers['storage'] = $this->storageUid;
+ return $this->getConnection()->delete($this->tableName, $identifiers);
}
- /**
- * @param string $folder
- *
- * @return array
- */
public function save(string $folder): array
{
$folderHash = sha1($folder);
@@ -175,11 +124,6 @@ public function save(string $folder): array
: ['folder_created' => $this->add($folder)];
}
- /**
- * @param string $folder
- *
- * @return int
- */
protected function add(string $folder): int
{
return $this->getConnection()->insert(
@@ -188,12 +132,6 @@ protected function add(string $folder): int
);
}
- /**
- * @param string $folder
- * @param string $folderHash
- *
- * @return int
- */
protected function update(string $folder, string $folderHash): int
{
return $this->getConnection()->update(
@@ -206,11 +144,6 @@ protected function update(string $folder, string $folderHash): int
);
}
- /**
- * @param string $folderPath
- *
- * @return string
- */
protected function computeParentFolder(string $folderPath): string
{
return dirname($folderPath) === '.'
@@ -218,11 +151,6 @@ protected function computeParentFolder(string $folderPath): string
: dirname($folderPath);
}
- /**
- * @param string $folderHash
- *
- * @return int
- */
protected function exists(string $folderHash): int
{
$query = $this->getQueryBuilder();
@@ -237,14 +165,9 @@ protected function exists(string $folderHash): int
)
);
- return (int)$query->execute()->fetchColumn(0);
+ return (int)$query->execute()->fetchOne(0);
}
- /**
- * @param string $folder
- *
- * @return array
- */
protected function getValues(string $folder): array
{
return [
@@ -258,12 +181,6 @@ protected function getValues(string $folder): array
];
}
- /**
- * @param string $key
- * @param array $cloudinaryResource
- *
- * @return string
- */
protected function getValue(string $key, array $cloudinaryResource): string
{
return isset($cloudinaryResource[$key])
@@ -271,9 +188,6 @@ protected function getValue(string $key, array $cloudinaryResource): string
: '';
}
- /**
- * @return object|QueryBuilder
- */
protected function getQueryBuilder(): QueryBuilder
{
/** @var ConnectionPool $connectionPool */
@@ -281,13 +195,11 @@ protected function getQueryBuilder(): QueryBuilder
return $connectionPool->getQueryBuilderForTable($this->tableName);
}
- /**
- * @return object|Connection
- */
protected function getConnection(): Connection
{
/** @var ConnectionPool $connectionPool */
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
return $connectionPool->getConnectionForTable($this->tableName);
}
+
}
diff --git a/Classes/Services/CloudinaryImageService.php b/Classes/Services/CloudinaryImageService.php
index e6b41cb..fe1f962 100644
--- a/Classes/Services/CloudinaryImageService.php
+++ b/Classes/Services/CloudinaryImageService.php
@@ -9,67 +9,62 @@
namespace Visol\Cloudinary\Services;
+use Cloudinary\Asset\Image;
+use Cloudinary\Transformation\ImageTransformation;
+use Cloudinary\Transformation\Scale;
+use Exception;
use TYPO3\CMS\Core\Resource\StorageRepository;
-use Cloudinary\Uploader;
-use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
-use TYPO3\CMS\Core\Log\LogLevel;
-use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Resource\File;
-use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use Visol\Cloudinary\Domain\Repository\ExplicitDataCacheRepository;
-use Visol\Cloudinary\Driver\CloudinaryDriver;
use Visol\Cloudinary\Utility\CloudinaryApiUtility;
-/**
- * Class CloudinaryImageService
- */
class CloudinaryImageService extends AbstractCloudinaryMediaService
{
- /**
- * @var ExplicitDataCacheRepository
- */
- protected $explicitDataCacheRepository;
+ // See "Max image megapixels" on https://cloudinary.com/pricing/compare-plans
+ const TRANSFORMATION_MAX_INPUT_PIXELS = 50_000_000;
- /**
- * @var StorageRepository
- */
- protected $storageRepository;
+ protected ExplicitDataCacheRepository $explicitDataCacheRepository;
+
+ protected ?StorageRepository $storageRepository = null;
protected array $defaultOptions = [
'type' => 'upload',
- 'resource_type' => 'image',
'fetch_format' => 'auto',
'quality' => 'auto',
];
- /**
- *
- */
public function __construct()
{
$this->explicitDataCacheRepository = GeneralUtility::makeInstance(ExplicitDataCacheRepository::class);
}
-
- /**
- * @param File $file
- * @param array $options
- *
- * @return array
- */
public function getExplicitData(File $file, array $options): array
{
$publicId = $this->getPublicIdForFile($file);
+ $options['resource_type'] = $this->getResourceTypeForFile($file);
$explicitData = $this->explicitDataCacheRepository->findByStorageAndPublicIdAndOptions($file->getStorage()->getUid(), $publicId, $options)['explicit_data'];
if (!$explicitData) {
- $this->initializeApi($file->getStorage());
- $explicitData = Uploader::explicit($publicId, $options);
+
+ // With Cloudinary API 2, we need to modify the way in which "responsive_breakpoints.transformation" are transmitted.
+ $apiOptions = $options;
+ if (isset($apiOptions['responsive_breakpoints']['transformation'])) {
+ // Check if we need to scale the image down, before applying image transformations
+ $prescaleTransformation = $this->getPrescaleTransformation($file);
+ $transformation = new ImageTransformation($prescaleTransformation);
+ foreach($apiOptions['responsive_breakpoints']['transformation'] as $parameters) {
+ $transformation->addActionFromQualifiers($parameters);
+ }
+ $apiOptions['responsive_breakpoints']['transformation'] = $transformation;
+ }
+
try {
+ $explicitData = (array)$this->getUploadApi($file->getStorage())->explicit($publicId, $apiOptions);
$this->explicitDataCacheRepository->save($file->getStorage()->getUid(), $publicId, $options, $explicitData);
- } catch (UniqueConstraintViolationException $e) {
+ } catch (Exception $e) {
+ $explicitData = [];
// ignore
}
}
@@ -77,34 +72,18 @@ public function getExplicitData(File $file, array $options): array
return $explicitData;
}
- /**
- * @param File $file
- * @param array $options
- *
- * @return array
- */
public function getResponsiveBreakpointData(File $file, array $options): array
{
$explicitData = $this->getExplicitData($file, $options);
- return $explicitData['responsive_breakpoints'][0]['breakpoints'];
+ return $explicitData['responsive_breakpoints'][0]['breakpoints'] ?? [];
}
- /**
- * @param array $breakpoints
- *
- * @return string
- */
public function getSrcsetAttribute(array $breakpoints): string
{
return implode(',' . PHP_EOL, $this->getSrcset($breakpoints));
}
- /**
- * @param array $breakpoints
- *
- * @return array
- */
public function getSrcset(array $breakpoints): array
{
$imageObjects = $this->getImageObjects($breakpoints);
@@ -116,11 +95,6 @@ public function getSrcset(array $breakpoints): array
return $srcset;
}
- /**
- * @param array $breakpoints
- *
- * @return string
- */
public function getSizesAttribute(array $breakpoints): string
{
$maxImageObject = $this->getImage($breakpoints, 'max');
@@ -128,9 +102,6 @@ public function getSizesAttribute(array $breakpoints): string
}
/**
- * @param array $breakpoints
- * @param string $functionName
- *
* @return mixed
*/
public function getImage(array $breakpoints, string $functionName)
@@ -166,15 +137,12 @@ public function getImageUrl(File $file, array $options = []): string
$publicId = $this->getPublicIdForFile($file);
- $this->initializeApi($file->getStorage());
- return \Cloudinary::cloudinary_url($publicId, $options);
+ $configuration = CloudinaryApiUtility::getConfiguration($file->getStorage());
+ return (string)Image::fromParams($publicId, $options)
+ ->configuration($configuration)
+ ->toUrl();
}
- /**
- * @param array $breakpoints
- *
- * @return array
- */
public function getImageObjects(array $breakpoints): array
{
$widthMap = [];
@@ -185,12 +153,6 @@ public function getImageObjects(array $breakpoints): array
return $widthMap;
}
- /**
- * @param array $settings
- * @param bool $enableResponsiveBreakpoints
- *
- * @return array
- */
public function generateOptionsFromSettings(array $settings, bool $enableResponsiveBreakpoints = true): array
{
$transformations = [];
@@ -204,10 +166,10 @@ public function generateOptionsFromSettings(array $settings, bool $enableRespons
) {
$transformations[] = [
'crop' => 'crop',
- 'width' => $settings['width'],
- 'height' => $settings['height'],
- 'x' => $settings['x'],
- 'y' => $settings['y'],
+ 'width' => (int)$settings['width'],
+ 'height' => (int)$settings['height'],
+ 'x' => (int)$settings['x'],
+ 'y' => (int)$settings['y'],
];
}
@@ -265,4 +227,23 @@ public function injectStorageRepository(StorageRepository $storageRepository): v
{
$this->storageRepository = $storageRepository;
}
+
+ /**
+ * Check if cloudinary needs to scale down the image before applying
+ * transformations. This function will return the required scaling
+ * transformation or null if no scaling is required.
+ */
+ protected function getPrescaleTransformation(File $file): ?Scale
+ {
+ $width = $file->getProperty('width') ?? 0;
+ $height = $file->getProperty('height') ?? 0;
+
+ if ($width * $height <= self::TRANSFORMATION_MAX_INPUT_PIXELS) {
+ return null;
+ }
+
+ // Calculate a width that allows the image to be processed
+ $maxWidth = (int)floor(sqrt(self::TRANSFORMATION_MAX_INPUT_PIXELS / ($height / $width)));
+ return Scale::limitFit($maxWidth);
+ }
}
diff --git a/Classes/Services/CloudinaryPathService.php b/Classes/Services/CloudinaryPathService.php
index a61c42d..ba73df3 100644
--- a/Classes/Services/CloudinaryPathService.php
+++ b/Classes/Services/CloudinaryPathService.php
@@ -9,55 +9,48 @@
* LICENSE.md file that was distributed with this source code.
*/
+use RuntimeException;
+use TYPO3\CMS\Core\Resource\ResourceStorage;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
+use Visol\Cloudinary\Driver\CloudinaryDriver;
+use Visol\Cloudinary\Utility\MimeTypeUtility;
-/**
- * Class CloudinaryPathService
- */
class CloudinaryPathService
{
+ protected ?ResourceStorage $storage;
- /**
- * @var array
- */
- protected $storageConfiguration;
+ protected array $storageConfiguration;
- /**
- * CloudinaryPathService constructor.
- *
- * @param array $storageConfiguration
- */
- public function __construct(array $storageConfiguration)
+ protected array $cachedCloudinaryResources = [];
+
+ public function __construct(array|ResourceStorage $storageObjectOrConfiguration)
{
- $this->storageConfiguration = $storageConfiguration;
+ if ($storageObjectOrConfiguration instanceof ResourceStorage) {
+ $this->storage = $storageObjectOrConfiguration;
+ $this->storageConfiguration = $this->storage->getConfiguration();
+ } else {
+ $this->storageConfiguration = $storageObjectOrConfiguration;
+ }
}
- /**
- * Cloudinary to FAL identifier
- *
- * @param array $cloudinaryResource
- *
- * @return string
- */
public function computeFileIdentifier(array $cloudinaryResource): string
{
- $fileParts = PathUtility::pathinfo($cloudinaryResource['public_id']);
+ $fileIdentifier = $cloudinaryResource['resource_type'] === 'raw'
+ ? $cloudinaryResource['public_id']
+ : $cloudinaryResource['public_id'] . '.' . $cloudinaryResource['format'];
- $extension = isset($fileParts['extension'])
- ? '' // We don't need the extension since it is already included in the public_id (resource_type => "raw")
- : '.' . $cloudinaryResource['format'];
-
- return self::stripBasePathFromIdentifier(
- DIRECTORY_SEPARATOR . $cloudinaryResource['public_id'] . $extension,
+ $fileIdentifier = self::stripBasePathFromIdentifier(
+ DIRECTORY_SEPARATOR . $fileIdentifier,
$this->getBasePath()
);
+
+ // ensure leading slash
+ $fileIdentifier = '/' . ltrim($fileIdentifier, './');
+
+ return $fileIdentifier;
}
- /**
- * @param string $cloudinaryFolder
- *
- * @return string
- */
public function computeFolderIdentifier(string $cloudinaryFolder): string
{
return self::stripBasePathFromIdentifier(
@@ -69,7 +62,6 @@ public function computeFolderIdentifier(string $cloudinaryFolder): string
/**
* Return the basePath.
* The basePath never has a trailing slash
- * @return string
*/
protected function getBasePath(): string
{
@@ -79,153 +71,91 @@ protected function getBasePath(): string
: '';
}
- /**
- * FAL to Cloudinary identifier
- *
- * @param string $fileIdentifier
- *
- * @return string
- */
public function computeCloudinaryPublicId(string $fileIdentifier): string
{
- $normalizedFileIdentifier = $this->guessIsImage($fileIdentifier) || $this->guessIsVideo($fileIdentifier)
- ? $this->stripExtension($fileIdentifier)
- : $fileIdentifier;
+ $fileExtension = $this->getFileExtension($fileIdentifier);
+ $publicId = in_array($fileExtension, CloudinaryDriver::$knownRawFormats)
+ ? $fileIdentifier
+ : $this->stripFileExtension($fileIdentifier);
- return $this->normalizeCloudinaryPath($normalizedFileIdentifier);
+ return $this->normalizeCloudinaryPublicId($publicId);
}
- /**
- * FAL to Cloudinary identifier
- *
- * @param string $folderIdentifier
- *
- * @return string
- */
public function computeCloudinaryFolderPath(string $folderIdentifier): string
{
- return $this->normalizeCloudinaryPath($folderIdentifier);
+ return $this->normalizeCloudinaryPublicId($folderIdentifier);
}
- /**
- * @param string $cloudinaryPath
- *
- * @return string
- */
- public function normalizeCloudinaryPath(string $cloudinaryPath): string
+ public function normalizeCloudinaryPublicId(string $cloudinaryPublicId): string
{
- $normalizedCloudinaryPath = trim($cloudinaryPath, DIRECTORY_SEPARATOR);
+ $normalizedCloudinaryPath = trim($cloudinaryPublicId, DIRECTORY_SEPARATOR);
$basePath = $this->getBasePath();
return $basePath
? trim($basePath . DIRECTORY_SEPARATOR . $normalizedCloudinaryPath, DIRECTORY_SEPARATOR)
: $normalizedCloudinaryPath;
}
- /**
- * @param array $fileInfo
- *
- * @return string
- */
public function getMimeType(array $fileInfo): string
{
- return isset($fileInfo['mime_type'])
- ? $fileInfo['mime_type']
- : '';
+ return $fileInfo['mime_type'] ?? '';
}
- /**
- * @param string $fileIdentifier
- *
- * @return string
- */
public function getResourceType(string $fileIdentifier): string
{
- $resourceType = 'raw';
- if ($this->guessIsImage($fileIdentifier)) {
- $resourceType = 'image';
- } elseif ($this->guessIsVideo($fileIdentifier)) {
- $resourceType = 'video';
+ try {
+ // Find the resource type from the cloudinary resource.
+ $cloudinaryResource = $this->getCloudinaryResource($fileIdentifier);
+ } catch (RuntimeException $e) {
+ $fileExtension = $this->getFileExtension($fileIdentifier);
+ $mimeType = MimeTypeUtility::guessMimeType($fileExtension);
+
+ // Get the primary resource type from the mime type such as image, video, audio, raw
+ $type = explode('/', $mimeType)[0];
+
+ // Equivalence table.
+ if ($type === 'application') {
+ $type = 'image';
+ } elseif ($type === 'text') {
+ $type = 'raw';
+ }
+ return $type;
}
-
- return $resourceType;
+ return $cloudinaryResource['resource_type'] ?? 'unknown';
}
- /**
- * @param array $cloudinaryResource
- *
- * @return string
- */
- public function guessMimeType(array $cloudinaryResource): string
+ protected function getCloudinaryResource(string $fileIdentifier): array
{
- $mimeType = '';
- if ($cloudinaryResource['format'] === 'pdf') {
- $mimeType = 'application/pdf';
- } elseif ($cloudinaryResource['format'] === 'jpg') {
- $mimeType = 'image/jpeg';
- } elseif ($cloudinaryResource['format'] === 'png') {
- $mimeType = 'image/png';
- } elseif ($cloudinaryResource['format'] === 'mp4') {
- $mimeType = 'video/mp4';
- }
- return $mimeType;
- }
+ $possiblePublicId = $this->stripFileExtension($fileIdentifier);
- /**
- * @param string $fileIdentifier
- *
- * @return bool
- */
- protected function guessIsVideo(string $fileIdentifier)
- {
- $extension = strtolower(PathUtility::pathinfo($fileIdentifier, PATHINFO_EXTENSION));
- $rawExtensions = [
- 'mp4',
- 'mov',
+ // We cache the resource for performance reasons.
+ if (!isset($this->cachedCloudinaryResources[$possiblePublicId])) {
- 'mp3', // As documented @see https://cloudinary.com/documentation/image_upload_api_reference
- ];
+ // We need to check whether the public id really exists.
+ $cloudinaryResourceService = GeneralUtility::makeInstance(
+ CloudinaryResourceService::class,
+ $this->storage
+ );
- return in_array($extension, $rawExtensions, true);
- }
+ $cloudinaryResource = $cloudinaryResourceService->getResource($possiblePublicId);
- /**
- * See if that is OK like that. The alternatives requires to "heavy" processing
- * like downloading the file to check the mime time or use the API SDK to fetch whether
- * we are in presence of an image.
- *
- * @param string $fileIdentifier
- *
- * @return bool
- */
- protected function guessIsImage(string $fileIdentifier)
- {
- $extension = strtolower(PathUtility::pathinfo($fileIdentifier, PATHINFO_EXTENSION));
- $imageExtensions = [
- 'png',
- 'jpe',
- 'jpeg',
- 'jpg',
- 'gif',
- 'bmp',
- 'ico',
- 'tiff',
- 'tif',
- 'svg',
- 'svgz',
- 'webp',
-
- 'pdf', // Cloudinary handles pdf as image
- ];
-
- return in_array($extension, $imageExtensions, true);
+ // Try to retrieve the cloudinary with the file identifier.
+ // That will be the case for raw resources.
+ if (!$cloudinaryResource) {
+ $cloudinaryResource = $cloudinaryResourceService->getResource($fileIdentifier);
+ }
+
+ // Houston, we have a problem. The public id does not exist, meaning the file does not exist.
+ if (!$cloudinaryResource) {
+ throw new RuntimeException('Cloudinary resource not found for ' . $fileIdentifier, 1623157880);
+ }
+
+ $this->cachedCloudinaryResources[$possiblePublicId] = $cloudinaryResource;
+ }
+
+ return $this->cachedCloudinaryResources[$possiblePublicId];
}
- /**
- * @param $filename
- *
- * @return string
- */
- protected function stripExtension(string $filename): string
+ protected function stripFileExtension(string $filename): string
{
$pathParts = PathUtility::pathinfo($filename);
@@ -236,6 +166,12 @@ protected function stripExtension(string $filename): string
return $pathParts['dirname'] . DIRECTORY_SEPARATOR . $pathParts['filename'];
}
+ protected function getFileExtension(string $filename): string
+ {
+ $pathInfo = PathUtility::pathinfo($filename);
+ return $pathInfo['extension'] ?? '';
+ }
+
public static function stripBasePathFromIdentifier(string $identifierWithBasePath, string $basePath): string
{
return preg_replace(
diff --git a/Classes/Services/CloudinaryResourceService.php b/Classes/Services/CloudinaryResourceService.php
index 8cfafa9..a223b8b 100644
--- a/Classes/Services/CloudinaryResourceService.php
+++ b/Classes/Services/CloudinaryResourceService.php
@@ -20,29 +20,15 @@
*/
class CloudinaryResourceService
{
- /**
- * @var string
- */
- protected $tableName = 'tx_cloudinary_resource';
-
- /**
- * @var ResourceStorage
- */
- protected $storage;
-
- /**
- * CloudinaryResourceService constructor.
- *
- * @param ResourceStorage $storage
- */
+ protected string $tableName = 'tx_cloudinary_cache_resources';
+
+ protected ResourceStorage $storage;
+
public function __construct(ResourceStorage $storage)
{
$this->storage = $storage;
}
- /**
- * @return int
- */
public function markAsMissing(): int
{
$values = ['missing' => 1];
@@ -50,11 +36,6 @@ public function markAsMissing(): int
return $this->getConnection()->update($this->tableName, $values, $identifier);
}
- /**
- * @param string $publicId
- *
- * @return array
- */
public function getResource(string $publicId): array
{
$query = $this->getQueryBuilder();
@@ -68,23 +49,16 @@ public function getResource(string $publicId): array
->setMaxResults(1);
$resource = $query->execute()->fetchAssociative();
- return $resource ? $resource : [];
+ return $resource ?: [];
}
- /**
- * @param string $folder
- * @param array $orderings
- * @param array $pagination
- * @param bool $recursive
- *
- * @return array
- */
public function getResources(
string $folder,
- array $orderings = [],
- array $pagination = [],
- bool $recursive = false
- ): array {
+ array $orderings = [],
+ array $pagination = [],
+ bool $recursive = false
+ ): array
+ {
$query = $this->getQueryBuilder();
$query
->select('*')
@@ -92,28 +66,22 @@ public function getResources(
->where($query->expr()->eq('storage', $this->storage->getUid()));
// We should handle recursion
- $expresion = $recursive
+ $expression = $recursive
? $query->expr()->like('folder', $query->expr()->literal($folder . '%'))
: $query->expr()->eq('folder', $query->expr()->literal($folder));
- $query->andWhere($expresion);
+ $query->andWhere($expression);
if ($orderings) {
$query->orderBy($orderings['fieldName'], $orderings['direction']);
}
- if ($pagination && (int) $pagination['maxResult'] > 0) {
- $query->setMaxResults((int) $pagination['maxResult']);
- $query->setFirstResult((int) $pagination['firstResult']);
+ if ($pagination && (int)$pagination['maxResult'] > 0) {
+ $query->setMaxResults((int)$pagination['maxResult']);
+ $query->setFirstResult((int)$pagination['firstResult']);
}
- return $query->execute()->fetchAll();
+ return $query->execute()->fetchAllAssociative();
}
- /**
- * @param string $folder
- * @param bool $recursive
- *
- * @return int
- */
public function count(string $folder, bool $recursive = false): int
{
$query = $this->getQueryBuilder();
@@ -128,14 +96,9 @@ public function count(string $folder, bool $recursive = false): int
: $query->expr()->eq('folder', $query->expr()->literal($folder));
$query->andWhere($expresion);
- return (int) $query->execute()->fetchColumn(0);
+ return (int)$query->execute()->fetchOne(0);
}
- /**
- * @param string $publicId
- *
- * @return int
- */
public function delete(string $publicId): int
{
$identifier['public_id'] = $publicId;
@@ -143,22 +106,12 @@ public function delete(string $publicId): int
return $this->getConnection()->delete($this->tableName, $identifier);
}
- /**
- * @param array $identifier
- *
- * @return int
- */
- public function deleteAll(array $identifier = []): int
+ public function deleteAll(array $identifiers = []): int
{
- $identifier['storage'] = $this->storage->getUid();
- return $this->getConnection()->delete($this->tableName, $identifier);
+ $identifiers['storage'] = $this->storage->getUid();
+ return $this->getConnection()->delete($this->tableName, $identifiers);
}
- /**
- * @param array $cloudinaryResource
- *
- * @return array
- */
public function save(array $cloudinaryResource): array
{
$publicIdHash = $this->getPublicIdHash($cloudinaryResource);
@@ -169,27 +122,24 @@ public function save(array $cloudinaryResource): array
$this->getCloudinaryFolderService()->save($folder);
}
- return $this->exists($publicIdHash)
- ? ['updated' => $this->update($cloudinaryResource, $publicIdHash), 'publicIdHash' => $publicIdHash]
- : ['created' => $this->add($cloudinaryResource), 'publicIdHash' => $publicIdHash];
+ $result = $this->exists($publicIdHash)
+ ? ['updated' => $this->update($cloudinaryResource, $publicIdHash),]
+ : ['created' => $this->add($cloudinaryResource),];
+
+ return array_merge(
+ $result,
+ [
+ 'publicIdHash' => $publicIdHash,
+ 'resource' => $cloudinaryResource,
+ ]
+ );
}
- /**
- * @param array $cloudinaryResource
- *
- * @return int
- */
protected function add(array $cloudinaryResource): int
{
return $this->getConnection()->insert($this->tableName, $this->getValues($cloudinaryResource));
}
- /**
- * @param array $cloudinaryResource
- * @param string $publicIdHash
- *
- * @return int
- */
protected function update(array $cloudinaryResource, string $publicIdHash): int
{
return $this->getConnection()->update($this->tableName, $this->getValues($cloudinaryResource), [
@@ -198,11 +148,6 @@ protected function update(array $cloudinaryResource, string $publicIdHash): int
]);
}
- /**
- * @param string $publicIdHash
- *
- * @return int
- */
protected function exists(string $publicIdHash): int
{
$query = $this->getQueryBuilder();
@@ -214,14 +159,9 @@ protected function exists(string $publicIdHash): int
$query->expr()->eq('public_id_hash', $query->expr()->literal($publicIdHash)),
);
- return (int) $query->execute()->fetchOne(0);
+ return (int)$query->execute()->fetchOne(0);
}
- /**
- * @param array $cloudinaryResource
- *
- * @return array
- */
protected function getValues(array $cloudinaryResource): array
{
$publicIdHash = $this->getPublicIdHash($cloudinaryResource);
@@ -232,16 +172,16 @@ protected function getValues(array $cloudinaryResource): array
'folder' => $this->getFolder($cloudinaryResource),
'filename' => $this->getFileName($cloudinaryResource),
'format' => $this->getValue('format', $cloudinaryResource),
- 'version' => (int) $this->getValue('version', $cloudinaryResource),
+ 'version' => (int)$this->getValue('version', $cloudinaryResource),
'resource_type' => $this->getValue('resource_type', $cloudinaryResource),
'type' => $this->getValue('type', $cloudinaryResource),
'created_at' => $this->getCreatedAt($cloudinaryResource),
'uploaded_at' => $this->getUpdatedAt($cloudinaryResource),
- 'bytes' => (int) $this->getValue('bytes', $cloudinaryResource),
- 'width' => (int) $this->getValue('width', $cloudinaryResource),
- 'height' => (int) $this->getValue('height', $cloudinaryResource),
- 'aspect_ratio' => (float) $this->getValue('aspect_ratio', $cloudinaryResource),
- 'pixels' => (int) $this->getValue('pixels', $cloudinaryResource),
+ 'bytes' => (int)$this->getValue('bytes', $cloudinaryResource),
+ 'width' => (int)$this->getValue('width', $cloudinaryResource),
+ 'height' => (int)$this->getValue('height', $cloudinaryResource),
+ 'aspect_ratio' => (float)$this->getValue('aspect_ratio', $cloudinaryResource),
+ 'pixels' => (int)$this->getValue('pixels', $cloudinaryResource),
'url' => $this->getValue('url', $cloudinaryResource),
'secure_url' => $this->getValue('secure_url', $cloudinaryResource),
'status' => $this->getValue('status', $cloudinaryResource),
@@ -255,85 +195,47 @@ protected function getValues(array $cloudinaryResource): array
];
}
- /**
- * @param string $key
- * @param array $cloudinaryResource
- *
- * @return string
- */
protected function getValue(string $key, array $cloudinaryResource): string
{
- return isset($cloudinaryResource[$key]) ? (string) $cloudinaryResource[$key] : '';
+ return isset($cloudinaryResource[$key]) ? (string)$cloudinaryResource[$key] : '';
}
- /**
- * @param array $cloudinaryResource
- *
- * @return string
- */
protected function getFileName(array $cloudinaryResource): string
{
return basename($this->getValue('public_id', $cloudinaryResource));
}
- /**
- * @param array $cloudinaryResource
- *
- * @return string
- */
protected function getFolder(array $cloudinaryResource): string
{
$folder = dirname($this->getValue('public_id', $cloudinaryResource));
return $folder === '.' ? '' : $folder;
}
- /**
- * @param array $cloudinaryResource
- *
- * @return string
- */
protected function getCreatedAt(array $cloudinaryResource): string
{
$createdAt = $this->getValue('created_at', $cloudinaryResource);
return date('Y-m-d h:i:s', strtotime($createdAt));
}
- /**
- * @param array $cloudinaryResource
- *
- * @return string
- */
protected function getPublicIdHash(array $cloudinaryResource): string
{
$publicId = $this->getValue('public_id', $cloudinaryResource);
return sha1($publicId);
}
- /**
- * @param array $cloudinaryResource
- *
- * @return string
- */
protected function getUpdatedAt(array $cloudinaryResource): string
{
$updatedAt = $this->getValue('updated_at', $cloudinaryResource)
- ? $this->getValue('updated_at', $cloudinaryResource)
- : $this->getValue('created_at', $cloudinaryResource);
+ ?: $this->getValue('created_at', $cloudinaryResource);
return date('Y-m-d h:i:s', strtotime($updatedAt));
}
- /**
- * @return object|CloudinaryFolderService
- */
protected function getCloudinaryFolderService(): CloudinaryFolderService
{
return GeneralUtility::makeInstance(CloudinaryFolderService::class, $this->storage->getUid());
}
- /**
- * @return object|QueryBuilder
- */
protected function getQueryBuilder(): QueryBuilder
{
/** @var ConnectionPool $connectionPool */
@@ -341,9 +243,6 @@ protected function getQueryBuilder(): QueryBuilder
return $connectionPool->getQueryBuilderForTable($this->tableName);
}
- /**
- * @return object|Connection
- */
protected function getConnection(): Connection
{
/** @var ConnectionPool $connectionPool */
diff --git a/Classes/Services/CloudinaryScanService.php b/Classes/Services/CloudinaryScanService.php
index 2d5eff7..cd9dbc5 100644
--- a/Classes/Services/CloudinaryScanService.php
+++ b/Classes/Services/CloudinaryScanService.php
@@ -8,8 +8,11 @@
* For the full copyright and license information, please read the
* LICENSE.md file that was distributed with this source code.
*/
+
+use Cloudinary\Api\Admin\AdminApi;
+use Cloudinary\Api\Search\SearchApi;
+use TYPO3\CMS\Core\Exception;
use TYPO3\CMS\Core\Log\Logger;
-use Cloudinary\Search;
use Symfony\Component\Console\Style\SymfonyStyle;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
@@ -20,9 +23,6 @@
use Visol\Cloudinary\Driver\CloudinaryDriver;
use Visol\Cloudinary\Utility\CloudinaryApiUtility;
-/**
- * Class CloudinaryScanService
- */
class CloudinaryScanService
{
@@ -33,25 +33,15 @@ class CloudinaryScanService
private const FAILED = 'failed';
private const FOLDER_DELETED = 'folder_deleted';
- /**
- * @var ResourceStorage
- */
- protected $storage;
-
- /**
- * @var CloudinaryPathService
- */
- protected $cloudinaryPathService;
-
- /**
- * @var string
- */
- protected $processedFolder = '_processed_';
-
- /**
- * @var array
- */
- protected $statistics = [
+ protected ResourceStorage $storage;
+
+ protected ?CloudinaryPathService $cloudinaryPathService = null;
+
+ protected string $processedFolder = '_processed_';
+
+ protected string $additionalExpression = '';
+
+ protected array $statistics = [
self::CREATED => 0,
self::UPDATED => 0,
self::DELETED => 0,
@@ -61,19 +51,8 @@ class CloudinaryScanService
self::FOLDER_DELETED => 0,
];
- /**
- * @var SymfonyStyle|null
- */
- protected $io;
-
- /**
- * CloudinaryScanService constructor.
- *
- * @param ResourceStorage $storage
- * @param SymfonyStyle|null $io
- *
- * @throws \Exception
- */
+ protected ?SymfonyStyle $io = null;
+
public function __construct(ResourceStorage $storage, SymfonyStyle $io = null)
{
if ($storage->getDriverType() !== CloudinaryDriver::DRIVER_TYPE) {
@@ -83,47 +62,46 @@ public function __construct(ResourceStorage $storage, SymfonyStyle $io = null)
$this->io = $io;
}
- /**
- * @return void
- */
- public function empty(): void
+ public function scanOne(string $publicId): array|null
{
- $this->getCloudinaryResourceService()->deleteAll();
- $this->getCloudinaryFolderService()->deleteAll();
+ try {
+ $resource = (array)$this->getAdminApi()->asset($publicId);
+ $result = $this->getCloudinaryResourceService()->save($resource);
+ } catch (Exception $exception) {
+ $result = null;
+ }
+ return $result;
}
- /**
- * @return array
- */
public function scan(): array
{
$this->preScan();
- // Before calling the Search API, make sure we are connected with the right cloudinary account
- $this->initializeApi();
-
$cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath(DIRECTORY_SEPARATOR);
+ // We initialize the array.
+ $expressions = [];
+
// Add a filter if the root directory contains a base path segment
// + remove _processed_ folder from the search
if ($cloudinaryFolder) {
$expressions[] = sprintf('folder=%s/*', $cloudinaryFolder);
$expressions[] = sprintf('NOT folder=%s/%s/*', $cloudinaryFolder, $this->processedFolder);
- } else {
- $expressions[] = sprintf('NOT folder=%s/*', $this->processedFolder);
}
- if ($this->io) {
- $this->io->writeln('Mirroring...' . chr(10));
+ if ($this->additionalExpression) {
+ $expressions[] = $this->additionalExpression;
}
+ $this->console('Mirroring...', true);
+
do {
$nextCursor = isset($response)
? $response['next_cursor']
: '';
- $this->log(
- '[API][SEARCH] Cloudinary\Search() - fetch resources from folder "%s" %s',
+ $this->info(
+ '[API] SearchApi - fetch resources from folder "%s" %s',
[
$cloudinaryFolder,
$nextCursor ? 'and cursor ' . $nextCursor : '',
@@ -133,34 +111,42 @@ public function scan(): array
]
);
- /** @var Search $search */
- $search = new Search();
-
- $response = $search
+ $response = $this->getSearchApi()
->expression(implode(' AND ', $expressions))
- ->sort_by('public_id', 'asc')
- ->max_results(500)
- ->next_cursor($nextCursor)
+ ->sortBy('public_id', 'asc')
+ ->maxResults(500)
+ ->nextCursor($nextCursor)
->execute();
if (is_array($response['resources'])) {
foreach ($response['resources'] as $resource) {
$fileIdentifier = $this->getCloudinaryPathService()->computeFileIdentifier($resource);
+
+ // Skip files in the processed folder is detected.
+ if (str_contains($fileIdentifier, $this->processedFolder)) {
+ $this->console('Skipped processed file ' . $fileIdentifier);
+ continue;
+ } elseif ($resource['resource_type'] === 'raw'
+ && !in_array($resource['format'], CloudinaryDriver::$knownRawFormats, true)) {
+ // Skip as well if the resource is of type raw
+ // We might have problem when indexing video such as .youtube and .vimeo
+ // which are not well-supported between cloudinary and typo3
+ $this->console('Skipped unknown raw file ' . $fileIdentifier);
+ continue;
+ }
+
try {
- if ($this->io) {
- $this->io->writeln($fileIdentifier);
- }
// Save mirrored file
$result = $this->getCloudinaryResourceService()->save($resource);
+ $isCreated = isset($result['created']) ? '(new)' : '';
+ $this->console('Scanned ' . $fileIdentifier . ' ' . $isCreated);
+
// Find if the file exists in sys_file already
if (!$this->fileExistsInStorage($fileIdentifier)) {
- if ($this->io) {
- $this->io->writeln('Indexing new file: ' . $fileIdentifier);
- $this->io->writeln('');
- }
+ $this->console('New file will be indexed in typo3 ' . $fileIdentifier, true);
// This will trigger a file indexation
$this->storage->getFile($fileIdentifier);
@@ -173,12 +159,9 @@ public function scan(): array
// In any case we can add a file to the counter.
// Later we can verify the total corresponds to the "created" + "updated" + "deleted" files
$this->statistics[self::TOTAL]++;
- }
- catch (\Exception $e) {
+ } catch (\Exception $e) {
$this->statistics[self::FAILED]++;
- if ($this->io) {
- $this->io->warning(sprintf('Error could not process "%s"', $fileIdentifier));
- }
+ $this->console(sprintf('Error could not process "%s"', $fileIdentifier));
// ignore
}
}
@@ -190,30 +173,19 @@ public function scan(): array
return $this->statistics;
}
- /**
- * @return void
- */
protected function preScan(): void
{
$this->getCloudinaryResourceService()->markAsMissing();
$this->getCloudinaryFolderService()->markAsMissing();
}
- /**
- * @return void
- */
protected function postScan(): void
{
- $identifier = ['missing' => 1];
- $this->statistics[self::DELETED] = $this->getCloudinaryResourceService()->deleteAll($identifier);
- $this->statistics[self::FOLDER_DELETED] = $this->getCloudinaryFolderService()->deleteAll($identifier);
+ $identifiers = ['missing' => 1];
+ $this->statistics[self::DELETED] = $this->getCloudinaryResourceService()->deleteAll($identifiers);
+ $this->statistics[self::FOLDER_DELETED] = $this->getCloudinaryFolderService()->deleteAll($identifiers);
}
- /**
- * @param string $fileIdentifier
- *
- * @return bool
- */
protected function fileExistsInStorage(string $fileIdentifier): bool
{
$query = $this->getQueryBuilder();
@@ -221,8 +193,8 @@ protected function fileExistsInStorage(string $fileIdentifier): bool
->from('sys_file')
->where(
$this->getQueryBuilder()->expr()->eq(
- 'identifier',
- $query->expr()->literal($fileIdentifier)
+ 'identifier_hash',
+ $query->expr()->literal($this->storage->hashFileIdentifier($fileIdentifier)),
),
$this->getQueryBuilder()->expr()->eq(
'storage',
@@ -230,12 +202,9 @@ protected function fileExistsInStorage(string $fileIdentifier): bool
)
);
- return (bool)$query->execute()->fetchColumn(0);
+ return (bool)$query->execute()->fetchOne(0);
}
- /**
- * @return object|QueryBuilder
- */
protected function getQueryBuilder(): QueryBuilder
{
/** @var ConnectionPool $connectionPool */
@@ -243,51 +212,39 @@ protected function getQueryBuilder(): QueryBuilder
return $connectionPool->getQueryBuilderForTable('sys_file');
}
- /**
- * @return void
- */
- protected function initializeApi()
- {
- CloudinaryApiUtility::initializeByConfiguration($this->storage->getConfiguration());
- }
-
- /**
- * @return object|CloudinaryResourceService
- */
protected function getCloudinaryResourceService(): CloudinaryResourceService
{
return GeneralUtility::makeInstance(CloudinaryResourceService::class, $this->storage);
}
- /**
- * @return object|CloudinaryFolderService
- */
protected function getCloudinaryFolderService(): CloudinaryFolderService
{
return GeneralUtility::makeInstance(CloudinaryFolderService::class, $this->storage->getUid());
}
- /**
- * @return CloudinaryPathService
- */
protected function getCloudinaryPathService(): CloudinaryPathService
{
if (!$this->cloudinaryPathService) {
$this->cloudinaryPathService = GeneralUtility::makeInstance(
CloudinaryPathService::class,
- $this->storage->getConfiguration()
+ $this->storage
);
}
return $this->cloudinaryPathService;
}
- /**
- * @param string $message
- * @param array $arguments
- * @param array $data
- */
- protected function log(string $message, array $arguments = [], array $data = [])
+ protected function getSearchApi(): SearchApi
+ {
+ return CloudinaryApiUtility::getCloudinary($this->storage)->searchApi();
+ }
+
+ protected function getAdminApi(): AdminApi
+ {
+ return CloudinaryApiUtility::getCloudinary($this->storage)->adminApi();
+ }
+
+ protected function info(string $message, array $arguments = [], array $data = []): void
{
/** @var Logger $logger */
$logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
@@ -297,4 +254,20 @@ protected function log(string $message, array $arguments = [], array $data = [])
$data
);
}
+
+ protected function console(string $message, $additionalBlankLine = false): void
+ {
+ if ($this->io) {
+ $this->io->writeln($message);
+ if ($additionalBlankLine) {
+ $this->io->writeln('');
+ }
+ }
+ }
+
+ public function setAdditionalExpression(string $additionalExpression): CloudinaryScanService
+ {
+ $this->additionalExpression = $additionalExpression;
+ return $this;
+ }
}
diff --git a/Classes/Services/CloudinaryTestConnectionService.php b/Classes/Services/CloudinaryTestConnectionService.php
index ab8f068..d696274 100644
--- a/Classes/Services/CloudinaryTestConnectionService.php
+++ b/Classes/Services/CloudinaryTestConnectionService.php
@@ -8,12 +8,13 @@
* For the full copyright and license information, please read the
* LICENSE.md file that was distributed with this source code.
*/
-use Cloudinary\Search;
+
+use Cloudinary\Api\Search\SearchApi;
+use Exception;
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
-use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
use Visol\Cloudinary\Utility\CloudinaryApiUtility;
@@ -51,10 +52,7 @@ public function test()
$messageQueue = $this->getMessageQueue();
$localizationPrefix = $this->languageFile . ':driverConfiguration.message.';
try {
- $this->initializeApi();
-
- $search = new Search();
- $search
+ $search = $this->getSearchApi()
->expression('folder=/')
->execute();
@@ -66,7 +64,7 @@ public function test()
FlashMessage::OK
);
$messageQueue->addMessage($message);
- } catch (\Exception $exception) {
+ } catch (Exception $exception) {
/** @var FlashMessage $message */
$message = GeneralUtility::makeInstance(
FlashMessage::class,
@@ -88,13 +86,9 @@ protected function getMessageQueue()
return $flashMessageService->getMessageQueueByIdentifier();
}
- /**
- * @return void
- */
- protected function initializeApi()
+ protected function getSearchApi(): SearchApi
{
- CloudinaryApiUtility::initializeByConfiguration($this->configuration);
+ return CloudinaryApiUtility::getCloudinary($this->configuration)->searchApi();
}
-
}
diff --git a/Classes/Services/CloudinaryUploadService.php b/Classes/Services/CloudinaryUploadService.php
index 96c68f0..144e2cc 100644
--- a/Classes/Services/CloudinaryUploadService.php
+++ b/Classes/Services/CloudinaryUploadService.php
@@ -14,40 +14,22 @@
use TYPO3\CMS\Core\Log\LogLevel;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Resource\File;
-use TYPO3\CMS\Core\Resource\FileInterface;
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use Visol\Cloudinary\CloudinaryFactory;
-/**
- * Class CloudinaryUploadService
- */
class CloudinaryUploadService
{
- /**
- * @var string
- */
- protected $emergencyFileIdentifier = '/typo3conf/ext/cloudinary/Resources/Public/Images/emergency-placeholder-image.png';
+ protected string $emergencyFileIdentifier = '/typo3conf/ext/cloudinary/Resources/Public/Images/emergency-placeholder-image.png';
- /**
- * @var ResourceStorage
- */
- protected $storage;
+ protected ResourceStorage $storage;
- /**
- * @param ResourceStorage $storage
- */
public function __construct(ResourceStorage $storage = null)
{
$this->storage = $storage ?: CloudinaryFactory::getDefaultStorage();
}
- /**
- * @param string $fileIdentifier
- *
- * @return File|FileInterface
- */
- public function uploadLocalFile(string $fileIdentifier)
+ public function uploadLocalFile(string $fileIdentifier): File
{
// Cleanup file identifier in case
$fileIdentifier = $this->cleanUp($fileIdentifier);
@@ -68,19 +50,16 @@ public function uploadLocalFile(string $fileIdentifier)
);
}
- /**
- * @param string $fileIdentifier
- */
- protected function cleanUp(string $fileIdentifier)
+ public function getEmergencyFile(): File
+ {
+ return $this->uploadLocalFile($this->emergencyFileIdentifier);
+ }
+
+ protected function cleanUp(string $fileIdentifier): string
{
return DIRECTORY_SEPARATOR . ltrim($fileIdentifier, DIRECTORY_SEPARATOR);
}
- /**
- * @param string $fileIdentifier
- *
- * @return bool
- */
protected function fileExists(string $fileIdentifier): bool
{
$fileNameAndPath =
@@ -88,12 +67,7 @@ protected function fileExists(string $fileIdentifier): bool
return is_file($fileNameAndPath);
}
- /**
- * @param string $message
- * @param array $arguments
- * @param array $data
- */
- protected function error(string $message, array $arguments = [], array $data = [])
+ protected function error(string $message, array $arguments = [], array $data = []): void
{
/** @var Logger $logger */
$logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
diff --git a/Classes/Services/CloudinaryVideoService.php b/Classes/Services/CloudinaryVideoService.php
index 4e6dd35..971b457 100644
--- a/Classes/Services/CloudinaryVideoService.php
+++ b/Classes/Services/CloudinaryVideoService.php
@@ -2,11 +2,13 @@
namespace Visol\Cloudinary\Services;
+use Cloudinary\Asset\Video;
use TYPO3\CMS\Core\Resource\File;
+use Visol\Cloudinary\Utility\CloudinaryApiUtility;
class CloudinaryVideoService extends AbstractCloudinaryMediaService
{
- protected $defaultOptions = [
+ protected array $defaultOptions = [
'type' => 'upload',
'resource_type' => 'video',
'fetch_format' => 'auto',
@@ -19,7 +21,10 @@ public function getVideoUrl(File $file, array $options = []): string
$publicId = $this->getPublicIdForFile($file);
- $this->initializeApi($file->getStorage());
- return \Cloudinary::cloudinary_url($publicId, $options);
+ $configuration = CloudinaryApiUtility::getConfiguration($file->getStorage());
+
+ return Video::fromParams($publicId, $options)
+ ->configuration($configuration)
+ ->toUrl();
}
}
diff --git a/Classes/Services/ConfigurationService.php b/Classes/Services/ConfigurationService.php
index 32fa820..51896f7 100644
--- a/Classes/Services/ConfigurationService.php
+++ b/Classes/Services/ConfigurationService.php
@@ -10,15 +10,11 @@
*/
-/**
- * Class ConfigurationService
- */
+use RuntimeException;
+
class ConfigurationService
{
- /**
- * @var array
- */
- protected $configuration = [];
+ protected array $configuration = [];
/**
* ConfigurationService constructor.
@@ -33,12 +29,12 @@ public function __construct(array $configuration)
*/
public function get(string $key): string
{
- $value = (string)$this->configuration[$key];
- if (preg_match('/^%(.*)%$/', $value, $matches)) {
+ $value = trim((string)$this->configuration[$key]);
+ if (preg_match('/^%env\((.*)\)%$/', $value, $matches) || preg_match('/^%(.*)%$/', $value, $matches)) {
$value = getenv($matches[1]);
if ($value === false) {
- throw new \RuntimeException(sprintf('No value found for environment variable "%s"', $matches[1]), 1626948978);
+ throw new RuntimeException(sprintf('No value found for environment variable "%s"', $matches[1]), 1626948978);
}
$value = (string)$value;
diff --git a/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php b/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php
index 9b4a564..67d8136 100644
--- a/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php
+++ b/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php
@@ -2,20 +2,15 @@
namespace Visol\Cloudinary\Services\Extractor;
-use Cloudinary;
-use Psr\Log\LoggerAwareInterface;
-use Psr\Log\LoggerAwareTrait;
-use Psr\Log\LogLevel;
-use TYPO3\CMS\Core\Resource;
+use Cloudinary\Api\Admin\AdminApi;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Index\ExtractorInterface;
use TYPO3\CMS\Core\Resource\ResourceStorage;
-use TYPO3\CMS\Core\Type\File\ImageInfo;
use TYPO3\CMS\Core\Utility\GeneralUtility;
-use Visol\Cloudinary\Driver\CloudinaryFastDriver;
+use Visol\Cloudinary\Driver\CloudinaryDriver;
use Visol\Cloudinary\Services\CloudinaryPathService;
use Visol\Cloudinary\Services\CloudinaryResourceService;
-use Visol\Cloudinary\Services\ConfigurationService;
+use Visol\Cloudinary\Utility\CloudinaryApiUtility;
class CloudinaryMetaDataExtractor implements ExtractorInterface
{
@@ -38,7 +33,7 @@ public function getFileTypeRestrictions(): array
*/
public function getDriverRestrictions(): array
{
- return [CloudinaryFastDriver::DRIVER_TYPE];
+ return [CloudinaryDriver::DRIVER_TYPE];
}
/**
@@ -71,18 +66,17 @@ public function extractMetaData(File $file, array $previousExtractedData = []):
$cloudinaryPathService = GeneralUtility::makeInstance(
CloudinaryPathService::class,
- $file->getStorage()->getConfiguration(),
+ $file->getStorage(),
);
$publicId = $cloudinaryPathService->computeCloudinaryPublicId($file->getIdentifier());
$resource = $cloudinaryResourceService->getResource($publicId);
// We are force calling cloudinary API
if (!$resource) {
- // Ask cloudinary to fetch the resource for us
- $this->initializeApi($file->getStorage());
- $api = new Cloudinary\Api();
- $resource = $api->resource($publicId);
+ // Ask cloudinary to fetch the resource for us
+ $resource = $this->getAdminApi($file->getStorage())
+ ->asset($publicId);
}
return [
@@ -91,16 +85,9 @@ public function extractMetaData(File $file, array $previousExtractedData = []):
];
}
- protected function initializeApi(ResourceStorage $storage): void
+ protected function getAdminApi(ResourceStorage $storage): AdminApi
{
- $configurationService = GeneralUtility::makeInstance(ConfigurationService::class, $storage->getConfiguration());
-
- Cloudinary::config([
- 'cloud_name' => $configurationService->get('cloudName'),
- 'api_key' => $configurationService->get('apiKey'),
- 'api_secret' => $configurationService->get('apiSecret'),
- 'timeout' => $configurationService->get('timeout'),
- 'secure' => true,
- ]);
+ return CloudinaryApiUtility::getCloudinary($storage)->adminApi();
}
+
}
diff --git a/Classes/Services/FileMoveService.php b/Classes/Services/FileMoveService.php
index d7cb39f..0302915 100644
--- a/Classes/Services/FileMoveService.php
+++ b/Classes/Services/FileMoveService.php
@@ -8,9 +8,11 @@
* For the full copyright and license information, please read the
* LICENSE.md file that was distributed with this source code.
*/
-use Cloudinary\Api;
-use Cloudinary\Uploader;
+
+use Cloudinary\Api\Admin\AdminApi;
+use Cloudinary\Api\Upload\UploadApi;
use Doctrine\DBAL\Driver\Connection;
+use Exception;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Resource\File;
@@ -18,31 +20,15 @@
use TYPO3\CMS\Core\Utility\GeneralUtility;
use Visol\Cloudinary\Utility\CloudinaryApiUtility;
-/**
- * Class FileMoveService
- */
class FileMoveService
{
- /**
- * @var string
- */
- protected $tableName = 'sys_file';
-
- /**
- * @var CloudinaryPathService
- */
- protected $cloudinaryPathService;
-
- /**
- * @param File $fileObject
- * @param ResourceStorage $targetStorage
- *
- * @return bool
- */
+ protected string $tableName = 'sys_file';
+
+ protected ?CloudinaryPathService $cloudinaryPathService = null;
+
public function fileExists(File $fileObject, ResourceStorage $targetStorage): bool
{
- $this->initializeApi($targetStorage);
$this->initializeCloudinaryService($targetStorage);
// Retrieve the Public Id based on the file identifier
@@ -50,23 +36,15 @@ public function fileExists(File $fileObject, ResourceStorage $targetStorage): bo
->computeCloudinaryPublicId($fileObject->getIdentifier());
try {
- $api = new Api();
- $resource = $api->resource($publicId);
+ $resource = $this->getAdminApi($targetStorage)->asset($publicId);
$fileExists = !empty($resource);
- } catch (\Exception $exception) {
+ } catch (Exception $exception) {
$fileExists = false;
}
return $fileExists;
}
- /**
- * @param File $fileObject
- * @param ResourceStorage $targetStorage
- * @param bool $removeFile
- *
- * @return bool
- */
#public function forceMove(File $fileObject, ResourceStorage $targetStorage, $removeFile = true): bool
#{
# $isUpdated = $isDeletedFromSourceStorage = false;
@@ -96,14 +74,7 @@ public function fileExists(File $fileObject, ResourceStorage $targetStorage): bo
# return $isUpdated && $isDeletedFromSourceStorage;
#}
- /**
- * @param File $fileObject
- * @param ResourceStorage $targetStorage
- * @param bool $removeFile
- *
- * @return bool
- */
- public function changeStorage(File $fileObject, ResourceStorage $targetStorage, $removeFile = true): bool
+ public function changeStorage(File $fileObject, ResourceStorage $targetStorage, bool $removeFile = true): bool
{
// Update the storage uid
$isMigrated = (bool)$this->updateFile(
@@ -121,9 +92,6 @@ public function changeStorage(File $fileObject, ResourceStorage $targetStorage,
return $isMigrated;
}
- /**
- * @param File $fileObject
- */
protected function ensureDirectoryExistence(File $fileObject)
{
@@ -134,11 +102,6 @@ protected function ensureDirectoryExistence(File $fileObject)
}
}
- /**
- * @param File $fileObject
- *
- * @return string
- */
protected function getAbsolutePath(File $fileObject): string
{
// Compute the absolute file name of the file to move
@@ -147,11 +110,6 @@ protected function getAbsolutePath(File $fileObject): string
return GeneralUtility::getFileAbsFileName($fileRelativePath);
}
- /**
- * @param File $fileObject
- * @param ResourceStorage $targetStorage
- * @param string $baseUrl
- */
public function cloudinaryUploadFile(
File $fileObject,
ResourceStorage $targetStorage,
@@ -162,7 +120,6 @@ public function cloudinaryUploadFile(
$this->ensureDirectoryExistence($fileObject);
- $this->initializeApi($targetStorage);
$fileIdentifier = $fileObject->getIdentifier();
$publicId = $this->getCloudinaryPathService()
@@ -182,23 +139,12 @@ public function cloudinaryUploadFile(
: $this->getAbsolutePath($fileObject);
// Upload the file
- $resource = Uploader::upload(
+ $this->getUploadApi($targetStorage)->upload(
$fileNameAndPath,
$options
);
}
- /**
- * @param ResourceStorage $targetStorage
- */
- protected function initializeApi(ResourceStorage $targetStorage)
- {
- CloudinaryApiUtility::initializeByConfiguration($targetStorage->getConfiguration());
- }
-
- /**
- * @return object|QueryBuilder
- */
protected function getQueryBuilder(): QueryBuilder
{
/** @var ConnectionPool $connectionPool */
@@ -206,9 +152,6 @@ protected function getQueryBuilder(): QueryBuilder
return $connectionPool->getQueryBuilderForTable($this->tableName);
}
- /**
- * @return object|Connection
- */
protected function getConnection(): Connection
{
/** @var ConnectionPool $connectionPool */
@@ -216,12 +159,6 @@ protected function getConnection(): Connection
return $connectionPool->getConnectionForTable($this->tableName);
}
- /**
- * @param File $fileObject
- * @param array $values
- *
- * @return int
- */
protected function updateFile(File $fileObject, array $values): int
{
$connection = $this->getConnection();
@@ -234,22 +171,27 @@ protected function updateFile(File $fileObject, array $values): int
);
}
- /**
- * @return object|CloudinaryPathService
- */
- protected function getCloudinaryPathService()
+ protected function getCloudinaryPathService(): CloudinaryPathService
{
return $this->cloudinaryPathService;
}
- /**
- * @param ResourceStorage $storage
- */
protected function initializeCloudinaryService(ResourceStorage $storage)
{
$this->cloudinaryPathService = GeneralUtility::makeInstance(
CloudinaryPathService::class,
- $storage->getStorageRecord()
+ $storage
);
}
+
+ protected function getUploadApi(ResourceStorage $storage): UploadApi
+ {
+ return CloudinaryApiUtility::getCloudinary($storage)->uploadApi();
+ }
+
+ protected function getAdminApi(ResourceStorage $storage): AdminApi
+ {
+ return CloudinaryApiUtility::getCloudinary($storage)->adminApi();
+ }
+
}
diff --git a/Classes/Services/ProcessingTaskConverter.php b/Classes/Services/ProcessingTaskConverter.php
new file mode 100644
index 0000000..dd012b8
--- /dev/null
+++ b/Classes/Services/ProcessingTaskConverter.php
@@ -0,0 +1,116 @@
+getConfiguration();
+
+ if (isset($processingConfiguration['maskImages'])) {
+ throw new NoCloudinaryTransformation('Unable to maskImages', 1712760855588);
+ }
+
+ $transformations = [];
+
+ $cropTransformation = $this->getCropTransformation($task);
+ if (isset($cropTransformation)) {
+ $transformations[] = $cropTransformation;
+ }
+
+ $scaleTransformation = $this->getScaleTransformation($task);
+ if (isset($scaleTransformation)) {
+ $transformations[] = $scaleTransformation;
+ }
+
+ return $transformations;
+ }
+
+ protected function getCropTransformation(TaskInterface $task): ?Crop
+ {
+ $processingConfiguration = $task->getConfiguration();
+ $crop = $processingConfiguration['crop'] ?? null;
+ if (!isset($crop)) {
+ return null;
+ }
+
+ if (!$crop instanceof Area) {
+ throw new NoCloudinaryTransformation('Crop is not of type area', 1712760863923);
+ }
+
+ return new Crop(
+ CropMode::CROP,
+ (int)$crop->getWidth(),
+ (int)$crop->getHeight(),
+ null,
+ (int)$crop->getOffsetLeft(),
+ (int)$crop->getOffsetTop(),
+ );
+ }
+
+ protected function getScaleTransformation(TaskInterface $task): ?Scale
+ {
+ $processingConfiguration = $task->getConfiguration();
+
+ $setProperties = array_filter(array_intersect_key($processingConfiguration, array_flip(['width', 'height'])));
+ $maxProperties = array_filter(array_intersect_key($processingConfiguration, array_flip(['maxWidth', 'maxHeight'])));
+ $minProperties = array_filter(array_intersect_key($processingConfiguration, array_flip(['minWidth', 'minHeight'])));
+
+ if (!empty($minProperties)) {
+ throw new NoCloudinaryTransformation('min* is not yet supported', 1712780611488);
+ }
+ if (empty(array_merge($maxProperties, $setProperties))) {
+ return null;
+ }
+
+ foreach ($maxProperties as $propertyName => $value) {
+ $equivalentSetPropertyValue = sprintf('%sm', (int)$value);
+ $equivalentSetPropertyName = strtolower(str_replace('max', '', $propertyName));
+ if (($setProperties[$equivalentSetPropertyName] ?? '') === $equivalentSetPropertyValue) {
+ unset($setProperties[$equivalentSetPropertyName]);
+ }
+ }
+
+ if (!(empty($setProperties) || empty($maxProperties))) {
+ throw new NoCloudinaryTransformation('max* and width/height at the same time is not yet supported', 1712780615883);
+ }
+
+ if (!empty($maxProperties)) {
+ $scaleTransformation = [
+ 'crop' => count($maxProperties) >= 2 ? CropMode::FIT : CropMode:: FILL,
+ ];
+ foreach ($maxProperties as $key => $value) {
+ $scaleTransformation[strtolower(str_replace('max', '', $key))] = (int)$value;
+ }
+
+ return Scale::fromParams($scaleTransformation);
+ }
+
+ $scaleTransformation = [
+ 'crop' => CropMode::SCALE,
+ ];
+
+ foreach (['width', 'height'] as $property) {
+ $stringValue = $processingConfiguration[$property] ?? '';
+ preg_match('/^(\d*)(c)?/', $stringValue, $matches);
+ $pixels = $matches[1] ?? 0;
+ if ((int)$pixels > 0) {
+ $scaleTransformation[$property] = $pixels;
+ }
+ if ($matches[2] === 'c') {
+ $scaleTransformation['crop'] = CropMode::FILL;
+ }
+ }
+
+ return Scale::fromParams($scaleTransformation);
+ }
+}
\ No newline at end of file
diff --git a/Classes/Slots/FileProcessingSlot.php b/Classes/Slots/FileProcessingSlot.php
deleted file mode 100644
index ca25c6e..0000000
--- a/Classes/Slots/FileProcessingSlot.php
+++ /dev/null
@@ -1,81 +0,0 @@
-isProcessed()) {
- return;
- }
-
- if (strpos($processedFile->getIdentifier() ?? '', 'PROCESSEDFILE' ) === 0) {
- return;
- }
-
- $options = [
- 'type' => 'upload',
- 'eager' => [
- [
- //'format' => 'jpg', // `Invalid transformation component - auto`
- 'fetch_format' => 'auto',
- 'quality' => 'auto:eco',
- 'width' => 64,
- 'height' => 64,
- 'crop' => 'fit',
- ]
- ]
- ];
-
- $explicitData = $this->getCloudinaryImageService()->getExplicitData($file, $options);
- $url = $explicitData['eager'][0]['secure_url'];
-
- $parts = parse_url($url);
- $processedFile->setName(basename($url));
- $processedFile->setIdentifier('PROCESSEDFILE' . $parts['path']);
-
- $processedFile->updateProperties([
- 'width' => $explicitData['eager'][0]['width'],
- 'height' => $explicitData['eager'][0]['height'],
- ]);
-
- /** @var $processedFileRepository ProcessedFileRepository */
- $processedFileRepository = GeneralUtility::makeInstance(ProcessedFileRepository::class);
- $processedFileRepository->add($processedFile);
- }
-
- /**
- * @return object|CloudinaryImageService
- */
- public function getCloudinaryImageService()
- {
- return GeneralUtility::makeInstance(CloudinaryImageService::class);
- }
-
-}
diff --git a/Classes/Utility/CloudinaryApiUtility.php b/Classes/Utility/CloudinaryApiUtility.php
index 9329aaf..db67c09 100644
--- a/Classes/Utility/CloudinaryApiUtility.php
+++ b/Classes/Utility/CloudinaryApiUtility.php
@@ -8,7 +8,13 @@
* For the full copyright and license information, please read the
* LICENSE.md file that was distributed with this source code.
*/
+
+use Cloudinary\Cloudinary;
+use Cloudinary\Configuration\Configuration;
+use Exception;
+use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
+use Visol\Cloudinary\Driver\CloudinaryDriver;
use Visol\Cloudinary\Services\ConfigurationService;
/**
@@ -17,23 +23,48 @@
class CloudinaryApiUtility
{
- public static function initializeByConfiguration(array $configuration)
+ public static function getCloudinary(ResourceStorage|array $storage): Cloudinary
{
+ return new Cloudinary(self::getConfiguration($storage));
+ }
+
+ public static function getConfiguration(ResourceStorage|array $storage): Configuration
+ {
+ return Configuration::instance(self::getArrayConfiguration($storage));
+ }
+
+ public static function getArrayConfiguration(ResourceStorage|array $storage): array
+ {
+ if (is_array($storage)) {
+ $storageConfiguration = $storage;
+ } else {
+ if ($storage->getDriverType() !== CloudinaryDriver::DRIVER_TYPE) {
+ // Check the file is stored on the right storage
+ // If not we should trigger an exception
+ $message = sprintf(
+ 'Wrong storage! Can not initialize with storage type "%s".',
+ $storage->getDriverType()
+ );
+ throw new Exception($message, 1590401459);
+ }
+ $storageConfiguration = $storage->getConfiguration();
+ }
+
/** @var ConfigurationService $configurationService */
$configurationService = GeneralUtility::makeInstance(
ConfigurationService::class,
- $configuration
+ $storageConfiguration
);
- \Cloudinary::config(
- [
- 'cloud_name' => $configurationService->get('cloudName'),
- 'api_key' => $configurationService->get('apiKey'),
- 'api_secret' => $configurationService->get('apiSecret'),
- 'timeout' => $configurationService->get('timeout'),
- 'secure' => true
- ]
- );
+ return [
+ 'cloud_name' => $configurationService->get('cloudName'),
+ 'api_key' => $configurationService->get('apiKey'),
+ 'api_secret' => $configurationService->get('apiSecret'),
+ 'timeout' => $configurationService->get('timeout'),
+ 'secure' => true,
+ 'private_cdn' => $configurationService->get('cname') !== '',
+ 'secure_distribution' => $configurationService->get('cname'),
+ ];
}
}
diff --git a/Classes/Utility/CloudinaryFileUtility.php b/Classes/Utility/CloudinaryFileUtility.php
new file mode 100644
index 0000000..388f317
--- /dev/null
+++ b/Classes/Utility/CloudinaryFileUtility.php
@@ -0,0 +1,37 @@
+ 'text/plain',
+ 'htm' => 'text/html',
+ 'html' => 'text/html',
+ 'php' => 'text/html',
+ 'css' => 'text/css',
+ 'js' => 'text/javascript',
+ 'csv' => 'text/comma-separated-values',
+ 'ics' => 'text/calendar',
+ 'log' => 'text/x-log',
+ 'zsh' => 'text/x-scriptzsh',
+ 'rtx' => 'text/richtext',
+ 'srt' => 'text/srt',
+ 'vcf' => 'text/x-vcard',
+ 'vtt' => 'text/vtt',
+ 'xsl' => 'text/xsl',
+
+ // images
+ 'png' => 'image/png',
+ 'jpe' => 'image/jpeg',
+ 'jpeg' => 'image/jpeg',
+ 'jpg' => 'image/jpeg',
+ 'gif' => 'image/gif',
+ 'bmp' => 'image/bmp',
+ 'ico' => 'image/vnd.microsoft.icon',
+ 'tiff' => 'image/tiff',
+ 'tif' => 'image/tiff',
+ 'svg' => 'image/svg+xml',
+ 'svgz' => 'image/svg+xml',
+ 'json' => 'text/json',
+ 'cdr' => 'image/cdr',
+
+ // audio
+ 'mp3' => 'audio/mpeg',
+ 'qt' => 'video/quicktime',
+ 'aac' => 'audio/x-acc',
+ 'ac3' => 'audio/ac3',
+ 'aif' => 'audio/aiff',
+ 'au' => 'audio/x-au',
+ 'flac' => 'audio/x-flac',
+ 'm4a' => 'audio/x-m4a',
+ 'mid' => 'audio/midi',
+ 'ra' => 'audio/x-realaudio',
+ 'ram' => 'audio/x-pn-realaudio',
+ 'rpm' => 'audio/x-pn-realaudio-plugin',
+ 'wma' => 'audio/x-ms-wma',
+
+ // video
+ 'youtube' => 'video/youtube',
+ 'vimeo' => 'video/vimeo',
+ 'mov' => 'video/quicktime',
+ 'movie' => 'video/x-sgi-movie',
+ 'mp4' => 'video/mp4',
+ 'mpeg' => 'video/mpeg',
+ 'ogg' => 'video/ogg',
+ 'rv' => 'video/vnd.rn-realvideo',
+ 'webm' => 'video/webm',
+ 'wmv' => 'video/x-ms-wmv',
+ '3g2' => 'video/3gpp2',
+ '3gp' => 'video/3gp',
+ 'avi' => 'video/avi',
+ 'f4v' => 'video/x-f4v',
+ 'flv' => 'video/x-flv',
+ 'jp2' => 'video/mj2',
+
+ // adobe
+ 'pdf' => 'application/pdf',
+ 'psd' => 'image/vnd.adobe.photoshop',
+ 'ai' => 'application/postscript',
+ 'eps' => 'application/postscript',
+ 'ps' => 'application/postscript',
+ 'xml' => 'application/xml',
+ 'swf' => 'application/x-shockwave-flash',
+
+ // ms office
+ 'doc' => 'application/msword',
+ 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'rtf' => 'application/rtf',
+ 'xls' => 'application/vnd.ms-excel',
+ 'xlsx' => 'application/vnd.ms-excel',
+ 'ppt' => 'application/vnd.ms-powerpoint',
+ 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+
+ // open office
+ 'odt' => 'application/vnd.oasis.opendocument.text',
+ 'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
+ 'odp' => 'application/vnd.oasis.opendocument.presentation',
+
+ // archives
+ 'zip' => 'application/zip',
+ 'rar' => 'application/x-rar-compressed',
+ 'exe' => 'application/x-msdownload',
+ 'msi' => 'application/x-msdownload',
+ 'cab' => 'application/vnd.ms-cab-compressed',
+
+ // other
+ '7zip' => 'application/x-compressed',
+ 'cpt' => 'application/mac-compactpro',
+ 'dcr' => 'application/x-director',
+ 'dvi' => 'application/x-dvi',
+ 'gpg' => 'application/gpg-keys',
+ 'gtar' => 'application/x-gtar',
+ 'gzip' => 'application/x-gzip',
+ 'kml' => 'application/vnd.google-earth.kml+xml',
+ 'kmz' => 'application/vnd.google-earth.kmz',
+ 'm4u' => 'application/vnd.mpegurl',
+ 'mif' => 'application/vnd.mif',
+ 'p10' => 'application/pkcs10',
+ 'p12' => 'application/x-pkcs12',
+ 'p7a' => 'application/x-pkcs7-signature',
+ 'p7c' => 'application/pkcs7-mime',
+ 'p7r' => 'application/x-pkcs7-certreqresp',
+ 'p7s' => 'application/pkcs7-signature',
+ 'pem' => 'application/x-pem-file',
+ 'pgp' => 'application/pgp',
+ 'sit' => 'application/x-stuffit',
+ 'smil' => 'application/smil',
+ 'tar' => 'application/x-tar',
+ 'tgz' => 'application/x-gzip-compressed',
+ 'vlc' => 'application/videolan',
+ 'wbxml' => 'application/wbxml',
+ 'wmlc' => 'application/wmlc',
+ 'xhtml' => 'application/xhtml+xml',
+ 'xl' => 'application/excel',
+ 'xspf' => 'application/xspf+xml',
+ 'z' => 'application/x-compress',
+
+ ];
+
+ return array_key_exists($fileExtension, $mimeTypes)
+ ? $mimeTypes[$fileExtension]
+ : 'application/octet-stream';
+ }
+}
diff --git a/Classes/ViewHelpers/CloudinaryImageDataViewHelper.php b/Classes/ViewHelpers/CloudinaryImageDataViewHelper.php
index 1652044..16bf77d 100644
--- a/Classes/ViewHelpers/CloudinaryImageDataViewHelper.php
+++ b/Classes/ViewHelpers/CloudinaryImageDataViewHelper.php
@@ -8,10 +8,15 @@
* For the full copyright and license information, please read the
* LICENSE.md file that was distributed with this source code.
*/
+
+use Exception;
+use InvalidArgumentException;
+use RuntimeException;
use TYPO3\CMS\Extbase\Service\ImageService;
use TYPO3\CMS\Core\Resource\ResourceStorage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
+use UnexpectedValueException;
use Visol\Cloudinary\Services\CloudinaryImageService;
use Visol\Cloudinary\Services\CloudinaryPathService;
use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
@@ -134,7 +139,7 @@ public function render(): string
'medianImage' => $this->getCloudinaryImageService()->getImage($breakpoints, 'median'),
'maxImage' => $this->getCloudinaryImageService()->getImage($breakpoints, 'max'),
];
- } catch (\Exception $e) {
+ } catch (Exception $e) {
$responsiveImageData = [
'images' => [
1 => [
@@ -151,11 +156,11 @@ public function render(): string
}
} catch (ResourceDoesNotExistException $e) {
// thrown if file does not exist
- } catch (\UnexpectedValueException $e) {
+ } catch (UnexpectedValueException $e) {
// thrown if a file has been replaced with a folder
- } catch (\RuntimeException $e) {
+ } catch (RuntimeException $e) {
// RuntimeException thrown if a file is outside of a storage
- } catch (\InvalidArgumentException $e) {
+ } catch (InvalidArgumentException $e) {
// thrown if file storage does not exist
}
@@ -178,7 +183,7 @@ protected function getCloudinaryPathService(ResourceStorage $storage)
{
return GeneralUtility::makeInstance(
CloudinaryPathService::class,
- $storage->getConfiguration()
+ $storage
);
}
diff --git a/Classes/ViewHelpers/CloudinaryImageViewHelper.php b/Classes/ViewHelpers/CloudinaryImageViewHelper.php
index 9db76b5..7c507e8 100644
--- a/Classes/ViewHelpers/CloudinaryImageViewHelper.php
+++ b/Classes/ViewHelpers/CloudinaryImageViewHelper.php
@@ -8,9 +8,14 @@
* For the full copyright and license information, please read the
* LICENSE.md file that was distributed with this source code.
*/
+
+use Exception;
+use InvalidArgumentException;
+use RuntimeException;
use TYPO3\CMS\Extbase\Service\ImageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
+use UnexpectedValueException;
use Visol\Cloudinary\Services\CloudinaryImageService;
use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
use TYPO3\CMS\Core\Resource\FileInterface;
@@ -120,7 +125,7 @@ public function render(): string
$this->tag->addAttribute('sizes', $cloudinarySizes);
$this->tag->addAttribute('srcset', $cloudinarySrcset);
$this->tag->addAttribute('src', $image->getPublicUrl());
- } catch (\Exception $e) {
+ } catch (Exception $e) {
$this->tag->addAttribute('src', $image->getPublicUrl());
}
@@ -134,11 +139,11 @@ public function render(): string
}
} catch (ResourceDoesNotExistException $e) {
// thrown if file does not exist
- } catch (\UnexpectedValueException $e) {
+ } catch (UnexpectedValueException $e) {
// thrown if a file has been replaced with a folder
- } catch (\RuntimeException $e) {
+ } catch (RuntimeException $e) {
// RuntimeException thrown if a file is outside of a storage
- } catch (\InvalidArgumentException $e) {
+ } catch (InvalidArgumentException $e) {
// thrown if file storage does not exist
}
diff --git a/Configuration/Backend/AjaxRoutes.php b/Configuration/Backend/AjaxRoutes.php
index 9413ccc..2fb09ab 100644
--- a/Configuration/Backend/AjaxRoutes.php
+++ b/Configuration/Backend/AjaxRoutes.php
@@ -1,8 +1,10 @@
[
'path' => '/cloudinary/add-files',
- 'target' => \Visol\Cloudinary\Controller\CloudinaryAjaxController::class . '::addFilesAction',
+ 'target' => CloudinaryAjaxController::class . '::addFilesAction',
],
];
diff --git a/Configuration/FlexForm/CloudinaryFlexForm.xml b/Configuration/FlexForm/CloudinaryFlexForm.xml
index 0a5d5e5..5a5cb5e 100644
--- a/Configuration/FlexForm/CloudinaryFlexForm.xml
+++ b/Configuration/FlexForm/CloudinaryFlexForm.xml
@@ -46,6 +46,14 @@
+
+
+
+
+ input
+
+
+
diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml
index 7acc094..e514373 100644
--- a/Configuration/Services.yaml
+++ b/Configuration/Services.yaml
@@ -7,6 +7,13 @@ services:
Visol\Cloudinary\:
resource: '../Classes/*'
+ Visol\Cloudinary\Command\CloudinaryApiCommand:
+ tags:
+ - name: 'console.command'
+ command: 'cloudinary:api'
+ schedulable: false
+ description: Interact with cloudinary api
+
Visol\Cloudinary\Command\CloudinaryCopyCommand:
tags:
- name: 'console.command'
@@ -28,10 +35,16 @@ services:
schedulable: false
description: Run a suite of Acceptance Tests.
+ Visol\Cloudinary\Command\CloudinaryFixImageDimensionCommand:
+ tags:
+ - name: 'console.command'
+ command: 'cloudinary:fix:image:dimension'
+ description: Fix missing width and height of image.
+
Visol\Cloudinary\Command\CloudinaryFixJpegCommand:
tags:
- name: 'console.command'
- command: 'cloudinary:fix'
+ command: 'cloudinary:fix:image:after-move'
schedulable: false
description: After "moving" files you should fix the jpeg extension. Consult README.md for more info.
@@ -42,9 +55,29 @@ services:
schedulable: false
description: Scan and warm up a cloudinary storage.
+ Visol\Cloudinary\Command\CloudinaryStorageListCommand:
+ tags:
+ - name: 'console.command'
+ command: 'cloudinary:storage:list'
+ schedulable: false
+ description: Will display all available storages configured for cloudinary
+
+ Visol\Cloudinary\Command\CloudinaryMetadataCommand:
+ tags:
+ - name: 'console.command'
+ command: 'cloudinary:metadata'
+ schedulable: false
+ description: Set metadata on cloudinary resources such as file reference and file usage.
+
Visol\Cloudinary\Command\CloudinaryQueryCommand:
tags:
- name: 'console.command'
command: 'cloudinary:query'
schedulable: false
description: Query a given storage such a list, count files or folders.
+
+ Visol\Cloudinary\EventHandlers\BeforeFileProcessingEventHandler:
+ tags:
+ - name: event.listener
+ identifier: 'cloudinary-before-file-processing-event-handler'
+ event: TYPO3\CMS\Core\Resource\Event\BeforeFileProcessingEvent
diff --git a/Configuration/TypoScript/setup.typoscript b/Configuration/TypoScript/setup.webhook.example.typoscript
similarity index 75%
rename from Configuration/TypoScript/setup.typoscript
rename to Configuration/TypoScript/setup.webhook.example.typoscript
index 757cf4b..bb27603 100644
--- a/Configuration/TypoScript/setup.typoscript
+++ b/Configuration/TypoScript/setup.webhook.example.typoscript
@@ -9,7 +9,6 @@ page_1573555440 {
xhtml_cleaning = 0
admPanel = 0
disableAllHeaderCode = 1
- additionalHeaders.10.header = Content-type:text/html
}
10 = COA_INT
10 {
@@ -18,10 +17,13 @@ page_1573555440 {
userFunc = TYPO3\CMS\Extbase\Core\Bootstrap->run
vendorName = Visol
extensionName = Cloudinary
- pluginName = Cache
+ pluginName = WebHook
+ settings {
+ storage = ### !!! Add a storage uid
+ }
switchableControllerActions {
- CloudinaryScan {
- 1 = scan
+ CloudinaryWebHook {
+ 1 = process
}
}
}
diff --git a/Documentation/backend-cloudinary-integration-01.png b/Documentation/backend-cloudinary-integration-01.png
new file mode 100644
index 0000000..6c7581b
Binary files /dev/null and b/Documentation/backend-cloudinary-integration-01.png differ
diff --git a/Documentation/driver-configuration-02.png b/Documentation/driver-configuration-02.png
deleted file mode 100644
index 3001b7f..0000000
Binary files a/Documentation/driver-configuration-02.png and /dev/null differ
diff --git a/Documentation/driver-configuration-03.png b/Documentation/driver-configuration-03.png
new file mode 100644
index 0000000..4c54e79
Binary files /dev/null and b/Documentation/driver-configuration-03.png differ
diff --git a/Documentation/extension-configuration-01.png b/Documentation/extension-configuration-01.png
new file mode 100644
index 0000000..af213bb
Binary files /dev/null and b/Documentation/extension-configuration-01.png differ
diff --git a/Makefile b/Makefile
index 2a60cfe..5fcd547 100644
--- a/Makefile
+++ b/Makefile
@@ -50,6 +50,14 @@ lint-summary:
lint-fix:
phpcbf
+## phpstan analyse
+phpstan:
+ php -d memory_limit=512M ./vendor/bin/phpstan analyse -c phpstan.neon
+
+## phpstan adjust baseline
+phpstan-baseline:
+ php -d memory_limit=512M ./vendor/bin/phpstan analyse -c phpstan.neon --generate-baseline
+
#######################
# PHPUnit
#######################
diff --git a/README.md b/README.md
index 81e98a0..8bba5cf 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ A TYPO3 extension that connect TYPO3 with [Cloudinary](cloudinary.com) services
by the means of a **Cloudinary Driver for FAL**.
The extension also provides various View Helpers to render images on the Frontend.
Cloudinary is a service provider dealing with images and videos.
-It offers various services among other:
+It offers various services among others:
* CDN for fast images and videos delivering
* Manipulation of images and videos such as cropping, resizing and much more...
@@ -44,19 +44,62 @@ For a new "file storage" record, then:
* Pick the **Cloudinary** driver in the driver dropdown menu.
* Fill in the requested fields. Password and secrets can be found from the [Cloudinary Console](https://cloudinary.com/console).
-
-
-
Once the record is saved, you should see a message telling the connection could be successfully established.
You can now head to the File module list.
Notice the first time you click on a folder in the File list module,
it will take some time since the images must be fetched and put into the cloudinary cache.
Notice you can also use environment variable to configure the storage.
-The environment variable should be surrounded by %. Example `%BUCKET_NAME%`
+The environment variable should be surrounded by %. Example `%env(BUCKET_NAME%)`

+Cloudinary integration as file picker
+-------------------------------------
+
+The extension is providing an integration with Cloudinary so that the editor can directly interact with the cloudinary library in the backend.
+When clicking on a button, a modal window will open up displaying the Cloudinary files directly.
+From there, the files can be inserted directly as file references.
+
+
+
+To enable this button, we should first configure the extension settings to display the
+desired button and storage.
+
+
+
+
+We can even take it a step further by enabling auto-login.
+A new field called authenticationEmail has been added to the storage configuration.
+By providing a configured email in Cloudinary, we can automatically log
+in when clicking on the button. Magic!
+
+
+
+
+Configuration TCEFORM
+---------------------
+
+We can configure the form in the backend to hide the default TYPO3 button,
+thus limiting backend user interaction with the Cloudinary library.
+Here is an example of such a configuration:
+
+```
+TCEFORM {
+ pages {
+ media {
+ config {
+ appearance {
+ fileUploadAllowed = 0
+ fileByUrlAllowed = 0
+ elementBrowserEnabled = 0
+ }
+ }
+ }
+ }
+}
+```
+
Logging
-------
@@ -69,7 +112,7 @@ tail -f public/typo3temp/var/logs/cloudinary.log
To decide: we now have log level INFO. We might consider "increasing" the level to "DEBUG".
-Caveats and trouble shooting
+Caveats and troubleshooting
----------------------------
* **Free** Cloudinary account allows 500 API request per day
@@ -156,7 +199,7 @@ many resources at once.
cld sync --push localFolder remoteFolder
```
-The extension provides also a tool to copy a bunch of files (restricted to images) from one storage to an another.
+The extension provides also a tool to copy a bunch of files (restricted to images) from one storage to another.
This can be achieved with this command:
```shell script
@@ -164,7 +207,7 @@ This can be achieved with this command:
# where 1 is the source storage (local)
# and 2 is the target storage (cloudinary)
-# Ouptut:
+# Output:
Copying 64 files from storage "fileadmin/ (auto-created)" (1) to "Cloudinary Storage (fabidule)" (2)
Copying /introduction/images/typo3-book-backend-login.png
Copying /introduction/images/content/content-quote.png
@@ -198,20 +241,30 @@ Available targets:
Web Hook
--------
-Whenever uploading or editing a file through the Cloudinary Manager you can configure an URL
-as a web hook to be called to invalidate the cache in TYPO3.
-This is highly recommended to keep the data consistent between Cloudinary and TYPO3.
+
+Whenever uploading or editing a file in the cloudinary library, you can configure in the cloudinary settings a URL to
+be called as a web hook. This is recommended to keep the data consistent between Cloudinary and TYPO3. When overriding
+or moving a file across folders, cloudinary will inform TYPO3 that something has changed.
+
+It will basically:
+
+* invalidate the processed files
+* invalidate the page cache where the file is involved.
+
```shell script
https://domain.tld/?type=1573555440
```
-**Beware**: Do not rename, move or delete files in the Cloudinary Media Library. TYPO3 will not know about the change.
-We may need to implement a web hook. For now, it is necessary to perform these action in the File module in the Backend.
+This, however, will not work out of the box and requires some manual configuration.
+Refer to the file ext:cloudinary/Configuration/TypoScript/setup.typoscript where we define a custom type.
+This is an example TypoScript file. Make sure that the file is loaded, and that you have defined a storage UID.
+Your system may contain multiple Cloudinary storages, and each web hook must refer to its own Cloudinary storage.
+Eventually you will end up having as many config as you have cloudinary storage.
Source of inspiration
---------------------
-Adapter for theleague php flysystem for Cloudinary
+Adapter for php flysystem for Cloudinary
https://github.com/flownative/flow-google-cloudstorage
diff --git a/Resources/Private/Language/backend.xlf b/Resources/Private/Language/backend.xlf
index 50a64ed..da9a4c9 100644
--- a/Resources/Private/Language/backend.xlf
+++ b/Resources/Private/Language/backend.xlf
@@ -14,10 +14,13 @@
Cloudinary API Secret
- A possible base path to store files in a parent directory. Example `production/`
+ A possible base path for storing files in a parent directory on cloudinary. Example `production/`
+
+
+ A possible CNAME to serve your assets from, instead of the default res.cloudinary.com domain name.
- Email used for authentication (auto-login) in file reference fields in TCEForm.
+ Email address used for authentication (auto-login) in file reference fields within TCEForm.
Cloudinary API timeout
@@ -31,9 +34,6 @@
Congratulations! Cloudinary is successfully connected to TYPO3.
-
- Cloudinary resources
-