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..0e9dbd5 100644
--- a/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php
+++ b/Classes/Backend/Form/Container/InlineCloudinaryControlContainer.php
@@ -12,14 +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() {
-
+ public function render()
+ {
// We load here the cloudinary library
/** @var AssetCollector $assetCollector */
$assetCollector = GeneralUtility::makeInstance(AssetCollector::class);
@@ -32,11 +32,7 @@ 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);
@@ -107,7 +103,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();
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/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..a56d8dc 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
@@ -66,21 +65,11 @@ protected function configure()
);
}
- /**
- * @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
@@ -94,19 +83,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$message .= 'https://cloudinary.com/console' . LF;
$message .= 'Strong advice! Take a free account to run the test suite';
$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
@@ -118,16 +107,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 +160,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 */
diff --git a/Classes/Command/CloudinaryApiCommand.php b/Classes/Command/CloudinaryApiCommand.php
new file mode 100644
index 0000000..444546f
--- /dev/null
+++ b/Classes/Command/CloudinaryApiCommand.php
@@ -0,0 +1,184 @@
+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
+ ' ;
+
+ 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('fileUid', '', InputOption::VALUE_OPTIONAL, 'File uid', '')
+ ->addOption('publicId', '', InputOption::VALUE_OPTIONAL, 'Cloudinary public id', '')
+ ->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;
+
+ if ($delete) {
+ // 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);
+ }
+
+ $this->initializeApi();
+ try {
+ if ($publicId) {
+ $resource = $this->getApi()->resource($publicId);
+ $this->log(var_export((array)$resource, true));
+ } elseif ($expression) {
+
+ $counter = 0;
+ do {
+ $nextCursor = isset($response)
+ ? $response['next_cursor']
+ : '';
+
+ /** @var Search $search */
+ $search = new Search();
+
+ $response = $search
+ ->expression($expression)
+ ->sort_by('public_id', 'asc')
+ ->max_results(100)
+ ->next_cursor($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->getApi()->delete_resources($_resources);
+ }
+ }
+ } while (!empty($response) && isset($response['next_cursor']));
+
+ } else {
+ $this->log('Nothing to do...');
+ }
+ } catch (\Exception $exception) {
+ $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 getApi()
+ {
+ // create a new instance upon each API call to avoid driver confusion
+ return new Api();
+ }
+
+ protected function initializeApi(): void
+ {
+ CloudinaryApiUtility::initializeByConfiguration($this->storage->getConfiguration());
+ }
+}
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/CloudinaryFixJpegCommand.php b/Classes/Command/CloudinaryFixJpegCommand.php
index 2b5f1fb..a99a3a3 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;
+ protected ResourceStorage $targetStorage;
- /**
- * @param InputInterface $input
- * @param OutputInterface $output
- */
- protected function initialize(InputInterface $input, OutputInterface $output)
+ protected string $tableName = 'sys_file';
+
+ protected function initialize(InputInterface $input, OutputInterface $output): void
{
$this->io = new SymfonyStyle($input, $output);
@@ -46,7 +39,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
{
$message = 'After "moving" files you should fix the jpeg extension. Consult README.md for more info.';
$this->setDescription($message)
@@ -58,24 +51,19 @@ protected function configure()
/**
* 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 +78,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if (!$response) {
$this->log('Script aborted');
- return 0;
+ return Command::SUCCESS;
}
}
@@ -106,20 +94,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..b9ba892
--- /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
+ $this->initializeApi();
+ foreach ($publicIdOptions as $publicId => $options) {
+ $this->log('Updating tags and metadata for public id ' . $publicId);
+ \Cloudinary\Uploader::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 initializeApi(): void
+ {
+ CloudinaryApiUtility::initializeByConfiguration($this->storage->getConfiguration());
+ }
+
+}
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/Controller/CloudinaryWebHookController.php b/Classes/Controller/CloudinaryWebHookController.php
new file mode 100644
index 0000000..20cf259
--- /dev/null
+++ b/Classes/Controller/CloudinaryWebHookController.php
@@ -0,0 +1,413 @@
+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));
+ $this->initializeApi();
+
+ foreach ($publicIds as $publicId) {
+
+ if ($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
+ \Cloudinary\Uploader::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));
+ $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)
+ ->setMaxResults(1)
+ ->executeStatement();
+ }
+
+ protected function getFile(array $cloudinaryResource): File
+ {
+ $fileIdentifier = $this->cloudinaryPathService->computeFileIdentifier($cloudinaryResource);
+ $tableName = 'sys_file';
+ $q = $this->getQueryBuilder($tableName);
+ $fileRecord = $q->select('*')
+ ->from($tableName)
+ ->where(
+ $q->expr()->eq('storage', $this->storage->getUid()),
+ $q->expr()->eq('identifier', $q->expr()->literal($fileIdentifier))
+ )
+ ->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 initializeApi(): void
+ {
+ CloudinaryApiUtility::initializeByConfiguration($this->storage->getConfiguration());
+ }
+
+}
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..e79954f 100644
--- a/Classes/Driver/CloudinaryDriver.php
+++ b/Classes/Driver/CloudinaryDriver.php
@@ -8,116 +8,66 @@
* For the full copyright and license information, please read the
* LICENSE.md file that was distributed with this source code.
*/
+
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;
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\Log\Logger;
+use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Type\File\FileInfo;
-use Visol\Cloudinary\Cache\CloudinaryTypo3Cache;
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\Utility\CloudinaryApiUtility;
+use Visol\Cloudinary\Services\CloudinaryTestConnectionService;
+use Visol\Cloudinary\Services\ConfigurationService;
+use Visol\Cloudinary\Utility\CloudinaryFileUtility;
+use Visol\Cloudinary\Utility\MimeTypeUtility;
-/**
- * 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 = '';
+ protected const ROOT_FOLDER_IDENTIFIER = '/';
- /**
- * @var array[]
- */
- protected $cachedCloudinaryResources = [];
+ protected const UNSAFE_FILENAME_CHARACTER_EXPRESSION = '\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF';
+
+ static public array $knownRawFormats = ['youtube', 'vimeo'];
/**
- * @var array
+ * 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 $cachedFolders = [];
+ protected string $baseUrl = '';
/**
* Object permissions are cached here in subarrays like:
* $identifier => ['r' => bool, 'w' => bool]
- *
- * @var array
- */
- protected $cachedPermissions = [];
-
- /**
- * Cache to avoid creating multiple local files since it is time consuming.
- * We must download the file.
- *
- * @var array
- */
- protected $localProcessingFiles = [];
-
- /**
- * @var ResourceStorage
- */
- protected $storage = null;
-
- /**
- * @var CharsetConverter
*/
- 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 +78,45 @@ 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
{
- return $this->resourceExists($fileIdentifier)
- ? $this->getCachedCloudinaryResource($fileIdentifier)['secure_url']
- : '';
+ if ($processedPath = $this->computeProcessedPath($identifier)) {
+ return 'https://res.cloudinary.com/' . $processedPath;
+ }
+
+ $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 = [])
+ protected function log(string $message, array $arguments = [], array $data = []): void
{
/** @var Logger $logger */
$logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
@@ -177,88 +128,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 +178,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,26 +256,21 @@ 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] Cloudinary\Uploader::upload() - add resource "%s"',
[$cloudinaryPublicId],
['addFile()'],
);
@@ -387,16 +279,17 @@ public function addFile($localFilePath, $targetFolderIdentifier, $newFileName =
$this->initializeApi();
// Upload the file
- $resource = Uploader::upload($localFilePath, [
+ $cloudinaryResource = Uploader::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,103 +298,84 @@ 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();
+ $targetFileIdentifier = $this->canonicalizeFolderIdentifierAndFileName($targetFolderIdentifier, $fileName);
// Before calling API, make sure we are connected with the right "bucket"
$this->initializeApi();
- Uploader::upload($this->getPublicUrl($fileIdentifier), [
+ $cloudinaryResource = Uploader::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,
- 'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier),
- 'overwrite' => true,
- ];
-
- // Flush the file cache entries
- $this->log('[CACHE] Flushed as replacing file', [], ['replaceFile']);
- $this->flushFileCache();
// Before calling the API, make sure we are connected with the right "bucket"
$this->initializeApi();
// Upload the file
- Uploader::upload($localFilePath, $options);
+ $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)
+ public function deleteFile($fileIdentifier): bool
{
- // Necessary to happen in an early stage.
- $this->log('[CACHE] Flushed as deleting file', [], ['deleteFile']);
- $this->flushFileCache();
-
$cloudinaryPublicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($fileIdentifier);
$this->log(
- '[API][DELETE] Cloudinary\Api::delete_resources - delete resource "%s"',
+ '[API] Cloudinary\Api::delete_resources - delete resource "%s"',
[$cloudinaryPublicId],
['deleteFile'],
);
@@ -510,47 +384,53 @@ public function deleteFile($fileIdentifier)
'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier),
]);
- $key = is_array($response['deleted']) ? key($response['deleted']) : '';
+ $isDeleted = false;
+
+ foreach ($response['deleted'] as $publicId => $status) {
+ if ($status === 'deleted') {
+ $isDeleted = (bool)$this->getCloudinaryResourceService()->delete($publicId);
+ }
+ }
- return is_array($response['deleted']) &&
- isset($response['deleted'][$key]) &&
- $response['deleted'][$key] === 'deleted';
+ 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] Cloudinary\Api::delete_resources_by_prefix() - folder "%s"',
[$cloudinaryFolder],
['deleteFolder'],
);
- $this->getApi()->delete_resources_by_prefix($cloudinaryFolder);
+ $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"',
+ '[API] Cloudinary\Api::delete_folder() - folder "%s"',
[$cloudinaryFolder],
['deleteFolder'],
);
- $this->getApi()->delete_folder($cloudinaryFolder);
- }
+ $response = $this->getApi()->delete_folder($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 +438,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 +450,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] Cloudinary\Api::createFolder() - folder "%s"', [$cloudinaryFolder], ['createFolder']);
+ $response = $this->getApi()->create_folder($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 +508,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 +532,72 @@ 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 = 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;
}
/**
- * 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);
+
+ // 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);
+ // After working so hard, delete the old empty folder.
+ $this->deleteFolder($folderIdentifier);
+ }
return $renamedFiles;
}
@@ -755,10 +609,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 +641,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 +654,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 +672,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);
@@ -867,215 +700,214 @@ public function isWithin($folderIdentifier, $identifier)
}
/**
- * 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),
+ );
+
+ // 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';
}
- if (!isset($this->cachedCloudinaryResources[$folderIdentifier])) {
- // Try to fetch from the cache
- $this->cachedCloudinaryResources[$folderIdentifier] = $this->getCache()->getCachedFiles($folderIdentifier);
+ $orderings = [
+ 'fieldName' => $parameters['sort'],
+ 'direction' => isset($parameters['reverse']) && (int)$parameters['reverse'] ? 'DESC' : 'ASC',
+ ];
- // If not found in TYPO3 cache, ask Cloudinary
- if (!is_array($this->cachedCloudinaryResources[$folderIdentifier])) {
- $this->cachedCloudinaryResources[$folderIdentifier] = $this->getCloudinaryResources($folderIdentifier);
- }
- }
+ $pagination = [
+ 'maxResult' => $numberOfItems,
+ 'firstResult' => (int)GeneralUtility::_GP('pointer'),
+ ];
- // Set default sorting
- $parameters = (array) GeneralUtility::_GP('SET');
- if (empty($parameters)) {
- $parameters['sort'] = 'file';
- $parameters['reverse'] = 0;
- }
+ $cloudinaryResources = $this->getCloudinaryResourceService()->getResources(
+ $cloudinaryFolder,
+ $orderings,
+ $pagination,
+ $recursive,
+ );
- // 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',
- );
- }
- }
+ // Generate list of folders for the file module.
+ $files = [];
+ foreach ($cloudinaryResources as $cloudinaryResource) {
+ // Compute file identifier
+ $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier(
+ $this->getCloudinaryPathService()->computeFileIdentifier($cloudinaryResource),
+ );
- // Pagination
- if ($numberOfItems > 0) {
- $files = array_slice(
- $this->cachedCloudinaryResources[$folderIdentifier],
- (int) GeneralUtility::_GP('pointer'),
- $numberOfItems,
+ $result = $this->applyFilterMethodsToDirectoryItem(
+ $filterCallbacks,
+ basename($fileIdentifier),
+ $fileIdentifier,
+ dirname($fileIdentifier),
);
- } else {
- $files = $this->cachedCloudinaryResources[$folderIdentifier];
+
+ 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 +917,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 +934,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 +948,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,316 +966,185 @@ public function sanitizeFileName($fileName, $charset = '')
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);
}
/**
- * 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 $folders;
+ return GeneralUtility::makeInstance(ExplicitDataCacheRepository::class);
}
- /**
- * @param string $folderIdentifier
- *
- * @return array
- */
- protected function getCloudinaryResources(string $folderIdentifier): array
+ protected function getProcessedFilePattern(): string
{
- $cloudinaryResources = [];
- $cloudinaryFolder = $this->getCloudinaryPathService()->computeCloudinaryFolderPath($folderIdentifier);
- if (!$cloudinaryFolder) {
- $cloudinaryFolder = self::ROOT_FOLDER_IDENTIFIER . '*';
- }
- // 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),
- );
+ return sprintf('/^PROCESSEDFILE\/(%s\/.*)/', $this->configurationService->get('cloudName'));
+ }
- // Compute folder identifier
- #$computedFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier(
- # GeneralUtility::dirname($fileIdentifier)
- #);
+ protected function isProcessedFile(string $identifier): bool
+ {
+ return (bool)preg_match($this->getProcessedFilePattern(), $identifier);
+ }
- // 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 computeProcessedPath(string $identifier): string|null
{
- $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;
+ $cloudinaryPath = null;
+ if (preg_match($this->getProcessedFilePattern(), $identifier, $matches)) {
+ [, $cloudinaryPath] = $matches;
}
- return $cloudinaryResource;
+ return $cloudinaryPath;
}
- /**
- * @param string $fileIdentifier
- *
- * @return array|false
- */
- protected function getCachedCloudinaryResource(string $fileIdentifier)
+ protected function isFileIdentifier(string $newFileIdentifier): bool
{
- $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier(GeneralUtility::dirname($fileIdentifier));
+ return str_contains($newFileIdentifier, DIRECTORY_SEPARATOR);
+ }
- // Warm up the cache!
- if (!isset($this->cachedCloudinaryResources[$folderIdentifier][$fileIdentifier])) {
- $this->getFilesInFolder($folderIdentifier, 0, -1);
- }
+ protected function canonicalizeFolderIdentifierAndFolderName(string $folderIdentifier, string $folderName): string
+ {
+ $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();
+ if (!$this->cloudinaryFolderService) {
+ $this->cloudinaryFolderService = GeneralUtility::makeInstance(
+ CloudinaryFolderService::class,
+ $this->storageUid,
+ );
+ }
- $this->cachedFolders = [];
+ return $this->cloudinaryFolderService;
}
- /**
- * @return void
- */
- protected function initializeApi()
+ protected function initializeApi(): void
{
- CloudinaryApiUtility::initializeByConfiguration($this->configuration);
+ 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()
+ protected function getApi(): Api
{
$this->initializeApi();
@@ -1460,18 +1154,4 @@ protected function getApi()
// Therefore it is better to create a new instance upon each API call to avoid driver confusion
return new Api();
}
-
- /**
- * @return CloudinaryTypo3Cache|object
- */
- protected function getCache()
- {
- if ($this->cloudinaryTypo3Cache === null) {
- $this->cloudinaryTypo3Cache = GeneralUtility::makeInstance(
- CloudinaryTypo3Cache::class,
- (int) $this->storageUid,
- );
- }
- return $this->cloudinaryTypo3Cache;
- }
}
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..9f9240c
--- /dev/null
+++ b/Classes/EventHandlers/BeforeFileProcessingEventHandler.php
@@ -0,0 +1,76 @@
+getDriver();
+ $processedFile = $event->getProcessedFile();
+ /** @var File $file */
+ $file = $event->getFile();
+
+ if (!$driver instanceof CloudinaryDriver) {
+ return;
+ }
+
+ if ($processedFile->isProcessed()) {
+ return;
+ }
+
+ if (str_starts_with($processedFile->getIdentifier(), 'PROCESSEDFILE')) {
+ return;
+ }
+
+ $explicitData = $this->getCloudinaryImageService()->getExplicitData(
+ $file,
+ [
+ 'type' => 'upload',
+ 'eager' => [
+ [
+ //'format' => 'jpg', // `Invalid transformation component - auto`
+ 'fetch_format' => 'auto',
+ 'quality' => 'auto:eco',
+ 'width' => 64,
+ 'height' => 64,
+ 'crop' => 'fit',
+ ]
+ ]
+ ]
+ );
+ $url = $explicitData['eager'][0]['secure_url'];
+
+ $parts = parse_url($url);
+ $path = $parts['path'] ?? '';
+ $processedFile->setName(basename($url));
+ $processedFile->setIdentifier('PROCESSEDFILE' . $path);
+
+ $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..83fa5fc 100644
--- a/Classes/Services/AbstractCloudinaryMediaService.php
+++ b/Classes/Services/AbstractCloudinaryMediaService.php
@@ -33,12 +33,6 @@ protected function initializeApi(ResourceStorage $storage): void
CloudinaryApiUtility::initializeByConfiguration($storage->getConfiguration());
}
- /**
- * @param File $file
- * @param array $options
- *
- * @return array
- */
public function getExplicitData(File $file, array $options): array
{
$publicId = $this->getPublicIdForFile($file);
@@ -58,12 +52,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 +63,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 +91,8 @@ public function getPublicIdForFile(File $file): string
}
// Compute the cloudinary public id
- $publicId = $this
+ return $this
->getCloudinaryPathService($file->getStorage())
->computeCloudinaryPublicId($file->getIdentifier());
- return $publicId;
}
}
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..b8f7840 100644
--- a/Classes/Services/CloudinaryImageService.php
+++ b/Classes/Services/CloudinaryImageService.php
@@ -12,29 +12,15 @@
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;
+ protected ExplicitDataCacheRepository $explicitDataCacheRepository;
- /**
- * @var StorageRepository
- */
- protected $storageRepository;
+ protected ?StorageRepository $storageRepository = null;
protected array $defaultOptions = [
'type' => 'upload',
@@ -43,21 +29,11 @@ class CloudinaryImageService extends AbstractCloudinaryMediaService
'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);
@@ -77,12 +53,6 @@ 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);
@@ -90,21 +60,11 @@ public function getResponsiveBreakpointData(File $file, array $options): array
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 +76,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 +83,6 @@ public function getSizesAttribute(array $breakpoints): string
}
/**
- * @param array $breakpoints
- * @param string $functionName
- *
* @return mixed
*/
public function getImage(array $breakpoints, string $functionName)
@@ -170,11 +122,6 @@ public function getImageUrl(File $file, array $options = []): string
return \Cloudinary::cloudinary_url($publicId, $options);
}
- /**
- * @param array $breakpoints
- *
- * @return array
- */
public function getImageObjects(array $breakpoints): array
{
$widthMap = [];
@@ -185,12 +132,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 = [];
diff --git a/Classes/Services/CloudinaryPathService.php b/Classes/Services/CloudinaryPathService.php
index a61c42d..5ac4b2b 100644
--- a/Classes/Services/CloudinaryPathService.php
+++ b/Classes/Services/CloudinaryPathService.php
@@ -9,55 +9,41 @@
* LICENSE.md file that was distributed with this source code.
*/
+use TYPO3\CMS\Core\Resource\ResourceStorage;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
+use Visol\Cloudinary\Driver\CloudinaryDriver;
-/**
- * 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']);
-
- $extension = isset($fileParts['extension'])
- ? '' // We don't need the extension since it is already included in the public_id (resource_type => "raw")
- : '.' . $cloudinaryResource['format'];
+ $fileIdentifier = $cloudinaryResource['resource_type'] === 'raw'
+ ? $cloudinaryResource['public_id']
+ : $cloudinaryResource['public_id'] . '.' . $cloudinaryResource['format'];
return self::stripBasePathFromIdentifier(
- DIRECTORY_SEPARATOR . $cloudinaryResource['public_id'] . $extension,
+ DIRECTORY_SEPARATOR . $fileIdentifier,
$this->getBasePath()
);
}
- /**
- * @param string $cloudinaryFolder
- *
- * @return string
- */
public function computeFolderIdentifier(string $cloudinaryFolder): string
{
return self::stripBasePathFromIdentifier(
@@ -69,7 +55,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 +64,74 @@ 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';
- }
-
- return $resourceType;
+ $cloudinaryResource = $this->getCloudinaryResource($fileIdentifier);
+ 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 +142,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..27840ad 100644
--- a/Classes/Services/CloudinaryScanService.php
+++ b/Classes/Services/CloudinaryScanService.php
@@ -8,6 +8,9 @@
* For the full copyright and license information, please read the
* LICENSE.md file that was distributed with this source code.
*/
+
+use Cloudinary\Api;
+use TYPO3\CMS\Core\Exception;
use TYPO3\CMS\Core\Log\Logger;
use Cloudinary\Search;
use Symfony\Component\Console\Style\SymfonyStyle;
@@ -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,18 +62,17 @@ 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->getApi()->resource($publicId);
+ $result = $this->getCloudinaryResourceService()->save($resource);
+ } catch (Exception $exception) {
+ $result = null;
+ }
+ return $result;
}
- /**
- * @return array
- */
public function scan(): array
{
$this->preScan();
@@ -104,25 +82,28 @@ public function scan(): array
$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(
+ $this->info(
'[API][SEARCH] Cloudinary\Search() - fetch resources from folder "%s" %s',
[
$cloudinaryFolder,
@@ -146,21 +127,32 @@ public function scan(): array
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 +165,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 +179,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();
@@ -230,12 +208,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 +218,43 @@ protected function getQueryBuilder(): QueryBuilder
return $connectionPool->getQueryBuilderForTable('sys_file');
}
- /**
- * @return void
- */
- protected function initializeApi()
+ protected function initializeApi(): void
{
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 getApi()
+ {
+ // Initialize and configure the API for each call
+ $this->initializeApi();
+
+ // create a new instance upon each API call to avoid driver confusion
+ return new Api();
+ }
+
+ protected function info(string $message, array $arguments = [], array $data = []): void
{
/** @var Logger $logger */
$logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
@@ -297,4 +264,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/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/Extractor/CloudinaryMetaDataExtractor.php b/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php
index 9b4a564..825f7fe 100644
--- a/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php
+++ b/Classes/Services/Extractor/CloudinaryMetaDataExtractor.php
@@ -12,7 +12,7 @@
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;
@@ -38,7 +38,7 @@ public function getFileTypeRestrictions(): array
*/
public function getDriverRestrictions(): array
{
- return [CloudinaryFastDriver::DRIVER_TYPE];
+ return [CloudinaryDriver::DRIVER_TYPE];
}
/**
@@ -71,7 +71,7 @@ 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);
diff --git a/Classes/Services/FileMoveService.php b/Classes/Services/FileMoveService.php
index d7cb39f..6a5bb28 100644
--- a/Classes/Services/FileMoveService.php
+++ b/Classes/Services/FileMoveService.php
@@ -8,6 +8,7 @@
* 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 Doctrine\DBAL\Driver\Connection;
@@ -18,28 +19,13 @@
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);
@@ -60,13 +46,6 @@ public function fileExists(File $fileObject, ResourceStorage $targetStorage): bo
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 +75,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 +93,6 @@ public function changeStorage(File $fileObject, ResourceStorage $targetStorage,
return $isMigrated;
}
- /**
- * @param File $fileObject
- */
protected function ensureDirectoryExistence(File $fileObject)
{
@@ -134,11 +103,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 +111,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,
@@ -188,17 +147,11 @@ public function cloudinaryUploadFile(
);
}
- /**
- * @param ResourceStorage $targetStorage
- */
protected function initializeApi(ResourceStorage $targetStorage)
{
CloudinaryApiUtility::initializeByConfiguration($targetStorage->getConfiguration());
}
- /**
- * @return object|QueryBuilder
- */
protected function getQueryBuilder(): QueryBuilder
{
/** @var ConnectionPool $connectionPool */
@@ -206,9 +159,6 @@ protected function getQueryBuilder(): QueryBuilder
return $connectionPool->getQueryBuilderForTable($this->tableName);
}
- /**
- * @return object|Connection
- */
protected function getConnection(): Connection
{
/** @var ConnectionPool $connectionPool */
@@ -216,12 +166,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 +178,16 @@ 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
);
}
}
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/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..51146d4 100644
--- a/Classes/ViewHelpers/CloudinaryImageDataViewHelper.php
+++ b/Classes/ViewHelpers/CloudinaryImageDataViewHelper.php
@@ -178,7 +178,7 @@ protected function getCloudinaryPathService(ResourceStorage $storage)
{
return GeneralUtility::makeInstance(
CloudinaryPathService::class,
- $storage->getConfiguration()
+ $storage
);
}
diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml
index 7acc094..8bb65ae 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'
@@ -42,9 +49,22 @@ services:
schedulable: false
description: Scan and warm up a cloudinary storage.
+ 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-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..0f7eb62 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...
@@ -57,6 +57,52 @@ The environment variable should be surrounded by %. Example `%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 +115,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 +202,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 +210,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 +244,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..fa1760f 100644
--- a/Resources/Private/Language/backend.xlf
+++ b/Resources/Private/Language/backend.xlf
@@ -31,9 +31,6 @@
Congratulations! Cloudinary is successfully connected to TYPO3.
-
- Cloudinary resources
-