Skip to content

Commit c1b6bcf

Browse files
committed
feat: Allow to move files from external storage folder to cloudinary folder
Instead of moving from storage to storage, allow to move files from folder to cloudinary folder.
1 parent 9a8ded5 commit c1b6bcf

5 files changed

Lines changed: 118 additions & 88 deletions

File tree

Classes/Command/AbstractCloudinaryCommand.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
* LICENSE.md file that was distributed with this source code.
1010
*/
1111

12-
use Doctrine\DBAL\Driver\Connection;
1312
use Symfony\Component\Console\Input\InputInterface;
1413
use Symfony\Component\Console\Style\SymfonyStyle;
1514
use Symfony\Component\Console\Command\Command;
15+
use TYPO3\CMS\Core\Database\Connection;
1616
use TYPO3\CMS\Core\Database\ConnectionPool;
1717
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
18+
use TYPO3\CMS\Core\Resource\Folder;
1819
use TYPO3\CMS\Core\Resource\ResourceStorage;
1920
use TYPO3\CMS\Core\Utility\GeneralUtility;
2021
use Visol\Cloudinary\Driver\CloudinaryDriver;
@@ -35,14 +36,15 @@ abstract class AbstractCloudinaryCommand extends Command
3536

3637
protected string $tableName = 'sys_file';
3738

38-
protected function getFiles(ResourceStorage $storage, InputInterface $input): array
39+
protected function getFiles(Folder $folder, InputInterface $input): array
3940
{
4041
$query = $this->getQueryBuilder($this->tableName);
4142
$query
4243
->select('*')
4344
->from($this->tableName)
4445
->where(
45-
$query->expr()->eq('storage', $storage->getUid()),
46+
$query->expr()->eq('storage', $folder->getStorage()->getUid()),
47+
$query->expr()->like('identifier', $query->createNamedParameter($query->escapeLikeWildcards($folder->getIdentifier()) . '%')),
4648
$query->expr()->eq('missing', 0)
4749
);
4850

Classes/Command/CloudinaryCopyCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
6767
return Command::INVALID;
6868
}
6969

70-
$files = $this->getFiles($this->sourceStorage, $input);
70+
$files = $this->getFiles($this->sourceStorage->getRootLevelFolder(), $input);
7171

7272
if (count($files) === 0) {
7373
$this->log('No files found, no work for me!');

Classes/Command/CloudinaryFixJpegCommand.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
8686
name = REPLACE(name, '.jpeg', '.jpg')
8787
WHERE storage = " . $this->targetStorage->getUid();
8888

89-
$connection->query($query)->execute();
89+
$connection->executeStatement($query);
9090

9191

9292
return Command::SUCCESS;

Classes/Command/CloudinaryMoveCommand.php

Lines changed: 90 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,14 @@
1010
*/
1111

1212
use Exception;
13+
use Psr\EventDispatcher\EventDispatcherInterface;
1314
use Symfony\Component\Console\Command\Command;
1415
use Symfony\Component\Console\Style\SymfonyStyle;
16+
use TYPO3\CMS\Core\Resource\Driver\DriverInterface;
17+
use TYPO3\CMS\Core\Resource\Event\AfterFileAddedEvent;
18+
use TYPO3\CMS\Core\Resource\Event\AfterFileMovedEvent;
19+
use TYPO3\CMS\Core\Resource\Folder;
20+
use TYPO3\CMS\Core\Resource\Index\Indexer;
1521
use TYPO3\CMS\Core\Resource\ResourceStorage;
1622
use TYPO3\CMS\Core\Utility\PathUtility;
1723
use Visol\Cloudinary\Services\FileMoveService;
@@ -31,13 +37,13 @@ class CloudinaryMoveCommand extends AbstractCloudinaryCommand
3137

3238
protected array $missingFiles = [];
3339

34-
protected ResourceStorage $sourceStorage;
35-
36-
protected ResourceStorage $targetStorage;
40+
public function __construct(
41+
protected ResourceFactory $resourceFactory,
42+
protected EventDispatcherInterface $eventDispatcher,
43+
) {
44+
parent::__construct();
45+
}
3746

38-
/**
39-
* Configure the command by defining the name, options and arguments
40-
*/
4147
protected function configure(): void
4248
{
4349
$message = 'Move bunch of images to a cloudinary storage. Consult the README.md for more info.';
@@ -60,34 +66,51 @@ protected function initialize(InputInterface $input, OutputInterface $output): v
6066
$this->io = new SymfonyStyle($input, $output);
6167

6268
$this->isSilent = $input->getOption('silent');
63-
64-
/** @var ResourceFactory $resourceFactory */
65-
$resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
66-
67-
$this->sourceStorage = $resourceFactory->getStorageObject($input->getArgument('source'));
68-
$this->targetStorage = $resourceFactory->getStorageObject($input->getArgument('target'));
6969
}
7070

7171
protected function execute(InputInterface $input, OutputInterface $output): int
7272
{
73-
if (!$this->checkDriverType($this->targetStorage)) {
73+
$sourceCombinedIdentifier = $input->getArgument('source');
74+
if (!is_string($sourceCombinedIdentifier)) {
75+
throw new \LogicException('source argument must be a string', 1749032224634);
76+
}
77+
$source = $this->resourceFactory->getFolderObjectFromCombinedIdentifier($sourceCombinedIdentifier);
78+
$sourceStorage = $source->getStorage();
79+
$sourceStorageDriver = $this->getStorageDriver($sourceStorage);
80+
81+
$targetCombinedIdentifier = $input->getArgument('target');
82+
if (!is_string($targetCombinedIdentifier)) {
83+
throw new \LogicException('target argument must be a string', 1749032230062);
84+
}
85+
$target = $this->resourceFactory->getFolderObjectFromCombinedIdentifier($targetCombinedIdentifier);
86+
$targetIndexer = GeneralUtility::makeInstance(Indexer::class, $target->getStorage());
87+
88+
$baseUrl = $input->getOption('base-url');
89+
if (!is_string($baseUrl)) {
90+
throw new \LogicException('Base URL must be a string');
91+
}
92+
93+
if (!$sourceStorage->hasHierarchicalIdentifiers()) {
94+
$this->log('Source storage must use hierarchical identifiers');
95+
return Command::INVALID;
96+
}
97+
if (!$this->checkDriverType($target->getStorage())) {
7498
$this->log('Look out! target storage is not of type "cloudinary"');
7599
return Command::INVALID;
76100
}
77101

78-
$files = $this->getFiles($this->sourceStorage, $input);
79-
102+
$files = $this->getFiles($source, $input);
80103
if (count($files) === 0) {
81104
$this->log('No files found, no work for me!');
82105
return Command::SUCCESS;
83106
}
84107

85108
$this->log('I will process %s files to be moved from storage "%s" (%s) to "%s" (%s)', [
86109
count($files),
87-
$this->sourceStorage->getUid(),
88-
$this->sourceStorage->getName(),
89-
$this->targetStorage->getUid(),
90-
$this->targetStorage->getName(),
110+
$source->getCombinedIdentifier(),
111+
$sourceStorage->getName(),
112+
$target->getCombinedIdentifier(),
113+
$target->getStorage()->getName(),
91114
]);
92115

93116
// A chance to the user to confirm the action
@@ -96,45 +119,48 @@ protected function execute(InputInterface $input, OutputInterface $output): int
96119

97120
if (!$response) {
98121
$this->log('Script aborted');
99-
100-
return Command::SUCCESS;
122+
return Command::SUCCESS;
101123
}
102124
}
103125

104-
/** @var ResourceFactory $resourceFactory */
105-
$resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
106-
107126
$counter = 0;
108127
foreach ($files as $file) {
109128
$this->log();
110129
$this->log('Starting migration with %s', [$file['identifier']]);
111130

112-
/** @var $fileObject */
113-
$fileObject = $resourceFactory->getFileObjectByStorageAndIdentifier($this->sourceStorage->getUid(), $file['identifier']);
131+
/** @var File $fileObject */
132+
$fileObject = $this->resourceFactory->getFileObject($file['uid'], $file);
133+
$sourceFileExists = $fileObject->exists();
114134

115135
if ($this->isFileSkipped($fileObject)) {
116136
$this->log('Skipping file ' . $fileObject->getIdentifier());
117137
// $this->skippedFiles[] = $fileObject->getIdentifier();
118138
continue;
119139
}
120140

121-
if ($this->getFileMoveService()->fileExists($fileObject, $this->targetStorage)) {
141+
if (! str_starts_with($fileObject->getIdentifier(), $source->getIdentifier())) {
142+
throw new \LogicException('file is not in source folder', 1748004982814);
143+
}
144+
145+
$newIdentifier = str_replace($source->getIdentifier(), $target->getIdentifier(), $fileObject->getIdentifier());
146+
147+
if ($this->getFileMoveService()->fileExists($target->getStorage(), $newIdentifier)) {
122148
$this->log('File has already been uploaded, good for us %s', [$fileObject->getIdentifier()]);
123149
} else {
124150
// Detect if the file is existing on storage "source" (1)
125-
if (!$fileObject->exists() && !$input->getOption('base-url')) {
151+
if (!$sourceFileExists && empty($baseUrl)) {
126152
$this->log('Missing file %s', [$fileObject->getIdentifier()], self::WARNING);
127153
// We could log the missing files
128154
$this->missingFiles[] = $fileObject->getIdentifier();
129155
continue;
130156
}
131157

132158
// Upload the file
133-
$this->log('Uploading file from %s%s', [$input->getOption('base-url'), $fileObject->getIdentifier()]);
159+
$this->log('Uploading file from %s%s', [$baseUrl, $fileObject->getIdentifier()]);
134160

135161
try {
136162
$start = microtime(true);
137-
$this->getFileMoveService()->cloudinaryUploadFile($fileObject, $this->targetStorage, $input->getOption('base-url'));
163+
$this->getFileMoveService()->cloudinaryUploadFile($fileObject, $target, $newIdentifier, $baseUrl);
138164
$timeElapsedSeconds = microtime(true) - $start;
139165
$this->log('File uploaded, Elapsed time %.3f', [$timeElapsedSeconds]);
140166
} catch (Exception $e) {
@@ -148,9 +174,29 @@ protected function execute(InputInterface $input, OutputInterface $output): int
148174
}
149175
}
150176

151-
// changing file storage and hard delete the file from the current storage
177+
// Update sys_file entry
178+
// See \TYPO3\CMS\Core\Resource\ResourceStorage::moveFile for reference
152179
$this->log('Changing storage for file %s', [$fileObject->getIdentifier()]);
153-
$this->getFileMoveService()->changeStorage($fileObject, $this->targetStorage);
180+
$oldIdentifier = $fileObject->getIdentifier();
181+
$oldFolder = $fileObject->getParentFolder();
182+
$fileObject->updateProperties(['storage' => $target->getStorage()->getUid(), 'identifier' => $newIdentifier]);
183+
$newFolder = $fileObject->getParentFolder();
184+
if (!$newFolder instanceof Folder) {
185+
throw new \LogicException('New folder must be a Folder', 1749032215642);
186+
}
187+
188+
// clean up processed files
189+
$this->eventDispatcher->dispatch(new AfterFileAddedEvent($fileObject, $newFolder));
190+
$targetIndexer->updateIndexEntry($fileObject);
191+
192+
// Delete the file from the source storage without deleting the sys_file record
193+
if ($sourceFileExists) {
194+
$sourceStorageDriver->deleteFile($oldIdentifier);
195+
$sourceFileExists = false;
196+
}
197+
198+
$this->eventDispatcher->dispatch(new AfterFileMovedEvent($fileObject, $newFolder, $oldFolder));
199+
154200
$counter++;
155201
}
156202
$this->log(LF);
@@ -208,4 +254,16 @@ protected function getFileMoveService(): FileMoveService
208254
{
209255
return GeneralUtility::makeInstance(FileMoveService::class);
210256
}
257+
258+
protected function getStorageDriver(ResourceStorage $storage): DriverInterface
259+
{
260+
$reflection = new \ReflectionClass($storage);
261+
$property = $reflection->getProperty('driver');
262+
$property->setAccessible(true);
263+
$driver = $property->getValue($storage);
264+
if (!$driver instanceof DriverInterface) {
265+
throw new \LogicException('Storage driver must implement DriverInterface', 1749032330406);
266+
}
267+
return $driver;
268+
}
211269
}

Classes/Services/FileMoveService.php

Lines changed: 21 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use TYPO3\CMS\Core\Database\ConnectionPool;
1717
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
1818
use TYPO3\CMS\Core\Resource\File;
19+
use TYPO3\CMS\Core\Resource\Folder;
1920
use TYPO3\CMS\Core\Resource\ResourceStorage;
2021
use TYPO3\CMS\Core\Utility\GeneralUtility;
2122
use Visol\Cloudinary\Utility\CloudinaryApiUtility;
@@ -27,22 +28,22 @@ class FileMoveService
2728

2829
protected ?CloudinaryPathService $cloudinaryPathService = null;
2930

30-
public function fileExists(File $fileObject, ResourceStorage $targetStorage): bool
31+
public function fileExists(ResourceStorage $storage, string $identifier): bool
3132
{
32-
$this->initializeCloudinaryService($targetStorage);
33+
$this->initializeCloudinaryService($storage);
3334

3435
// Retrieve the Public Id based on the file identifier
35-
$publicId = $this->getCloudinaryPathService()
36-
->computeCloudinaryPublicId($fileObject->getIdentifier());
36+
$publicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($identifier);
3737

3838
try {
39-
$resource = $this->getAdminApi($targetStorage)->asset($publicId);
40-
$fileExists = !empty($resource);
39+
$resource = (array)$this->getAdminApi($storage)->asset($publicId);
40+
41+
// update resource index
42+
(new CloudinaryResourceService($storage))->save((array)$resource);
43+
return true;
4144
} catch (Exception $exception) {
42-
$fileExists = false;
45+
return false;
4346
}
44-
45-
return $fileExists;
4647
}
4748

4849
#public function forceMove(File $fileObject, ResourceStorage $targetStorage, $removeFile = true): bool
@@ -74,34 +75,6 @@ public function fileExists(File $fileObject, ResourceStorage $targetStorage): bo
7475
# return $isUpdated && $isDeletedFromSourceStorage;
7576
#}
7677

77-
public function changeStorage(File $fileObject, ResourceStorage $targetStorage, bool $removeFile = true): bool
78-
{
79-
// Update the storage uid
80-
$isMigrated = (bool)$this->updateFile(
81-
$fileObject,
82-
[
83-
'storage' => $targetStorage->getUid(),
84-
]
85-
);
86-
87-
if ($removeFile) {
88-
// Delete the file form the local storage
89-
$isMigrated = unlink($this->getAbsolutePath($fileObject));
90-
}
91-
92-
return $isMigrated;
93-
}
94-
95-
protected function ensureDirectoryExistence(File $fileObject)
96-
{
97-
98-
// Make sure the directory exists
99-
$directory = dirname($this->getAbsolutePath($fileObject));
100-
if (!is_dir($directory)) {
101-
GeneralUtility::mkdir_deep($directory);
102-
}
103-
}
104-
10578
protected function getAbsolutePath(File $fileObject): string
10679
{
10780
// Compute the absolute file name of the file to move
@@ -112,37 +85,34 @@ protected function getAbsolutePath(File $fileObject): string
11285

11386
public function cloudinaryUploadFile(
11487
File $fileObject,
115-
ResourceStorage $targetStorage,
88+
Folder $targetFolder,
89+
string $newIdentifier,
11690
string $baseUrl = ''
11791
): void {
118-
92+
$targetStorage = $targetFolder->getStorage();
11993
$this->initializeCloudinaryService($targetStorage);
120-
121-
$this->ensureDirectoryExistence($fileObject);
122-
123-
124-
$fileIdentifier = $fileObject->getIdentifier();
125-
$publicId = $this->getCloudinaryPathService()
126-
->computeCloudinaryPublicId($fileIdentifier);
94+
$publicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($newIdentifier);
12795

12896
$options = [
12997
'public_id' => basename($publicId),
13098
'folder' => $this->getCloudinaryPathService()
13199
->computeCloudinaryFolderPath(
132-
$fileObject->getParentFolder()->getIdentifier()
100+
dirname($newIdentifier)
133101
),
134-
'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier),
102+
'resource_type' => $this->getCloudinaryPathService()->getResourceType($newIdentifier),
135103
'overwrite' => true,
136104
];
137105
$fileNameAndPath = $baseUrl
138-
? rtrim($baseUrl, DIRECTORY_SEPARATOR) . $fileIdentifier
139-
: $this->getAbsolutePath($fileObject);
106+
? rtrim($baseUrl, DIRECTORY_SEPARATOR) . $fileObject->getIdentifier()
107+
: ($fileObject->getPublicUrl() ?: $fileObject->getForLocalProcessing(false));
140108

141109
// Upload the file
142-
$this->getUploadApi($targetStorage)->upload(
110+
$response = $this->getUploadApi($targetStorage)->upload(
143111
$fileNameAndPath,
144112
$options
145113
);
114+
$resource = (array)$response;
115+
(new CloudinaryResourceService($targetStorage))->save($resource);
146116
}
147117

148118
protected function getQueryBuilder(): QueryBuilder

0 commit comments

Comments
 (0)