Skip to content

Commit bddd62e

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 e091429 commit bddd62e

File tree

4 files changed

+92
-82
lines changed

4 files changed

+92
-82
lines changed

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(), Connection::PARAM_STR) . '%')),
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/CloudinaryMoveCommand.php

Lines changed: 67 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@
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\Index\Indexer;
1520
use TYPO3\CMS\Core\Resource\ResourceStorage;
1621
use TYPO3\CMS\Core\Utility\PathUtility;
1722
use Visol\Cloudinary\Services\FileMoveService;
@@ -31,13 +36,13 @@ class CloudinaryMoveCommand extends AbstractCloudinaryCommand
3136

3237
protected array $missingFiles = [];
3338

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

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

6267
$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'));
6968
}
7069

7170
protected function execute(InputInterface $input, OutputInterface $output): int
7271
{
73-
if (!$this->checkDriverType($this->targetStorage)) {
72+
$source = $this->resourceFactory->getFolderObjectFromCombinedIdentifier($input->getArgument('source'));
73+
$sourceStorage = $source->getStorage();
74+
$sourceStorageDriver = $this->getStorageDriver($sourceStorage);
75+
76+
$target = $this->resourceFactory->getFolderObjectFromCombinedIdentifier($input->getArgument('target'));
77+
$targetIndexer = GeneralUtility::makeInstance(Indexer::class, $target->getStorage());
78+
79+
if (!$sourceStorage->hasHierarchicalIdentifiers()) {
80+
$this->log('Source storage must use hierarchical identifiers');
81+
return Command::INVALID;
82+
}
83+
if (!$this->checkDriverType($target->getStorage())) {
7484
$this->log('Look out! target storage is not of type "cloudinary"');
7585
return Command::INVALID;
7686
}
7787

78-
$files = $this->getFiles($this->sourceStorage, $input);
79-
88+
$files = $this->getFiles($source, $input);
8089
if (count($files) === 0) {
8190
$this->log('No files found, no work for me!');
8291
return Command::SUCCESS;
8392
}
8493

8594
$this->log('I will process %s files to be moved from storage "%s" (%s) to "%s" (%s)', [
8695
count($files),
87-
$this->sourceStorage->getUid(),
88-
$this->sourceStorage->getName(),
89-
$this->targetStorage->getUid(),
90-
$this->targetStorage->getName(),
96+
$source->getCombinedIdentifier(),
97+
$sourceStorage->getName(),
98+
$target->getCombinedIdentifier(),
99+
$target->getStorage()->getName(),
91100
]);
92101

93102
// A chance to the user to confirm the action
@@ -96,33 +105,36 @@ protected function execute(InputInterface $input, OutputInterface $output): int
96105

97106
if (!$response) {
98107
$this->log('Script aborted');
99-
100-
return Command::SUCCESS;
108+
return Command::SUCCESS;
101109
}
102110
}
103111

104-
/** @var ResourceFactory $resourceFactory */
105-
$resourceFactory = GeneralUtility::makeInstance(ResourceFactory::class);
106-
107112
$counter = 0;
108113
foreach ($files as $file) {
109114
$this->log();
110115
$this->log('Starting migration with %s', [$file['identifier']]);
111116

112-
/** @var $fileObject */
113-
$fileObject = $resourceFactory->getFileObjectByStorageAndIdentifier($this->sourceStorage->getUid(), $file['identifier']);
117+
/** @var File $fileObject */
118+
$fileObject = $this->resourceFactory->getFileObject($file['uid'], $file);
119+
$sourceFileExists = $fileObject->exists();
114120

115121
if ($this->isFileSkipped($fileObject)) {
116122
$this->log('Skipping file ' . $fileObject->getIdentifier());
117123
// $this->skippedFiles[] = $fileObject->getIdentifier();
118124
continue;
119125
}
120126

121-
if ($this->getFileMoveService()->fileExists($fileObject, $this->targetStorage)) {
127+
if (! str_starts_with($fileObject->getIdentifier(), $source->getIdentifier())) {
128+
throw new \LogicException('file is not in source folder', 1748004982814);
129+
}
130+
131+
$newIdentifier = str_replace($source->getIdentifier(), $target->getIdentifier(), $fileObject->getIdentifier());
132+
133+
if ($this->getFileMoveService()->fileExists($target->getStorage(), $newIdentifier)) {
122134
$this->log('File has already been uploaded, good for us %s', [$fileObject->getIdentifier()]);
123135
} else {
124136
// Detect if the file is existing on storage "source" (1)
125-
if (!$fileObject->exists() && !$input->getOption('base-url')) {
137+
if (!$sourceFileExists && !$input->getOption('base-url')) {
126138
$this->log('Missing file %s', [$fileObject->getIdentifier()], self::WARNING);
127139
// We could log the missing files
128140
$this->missingFiles[] = $fileObject->getIdentifier();
@@ -134,7 +146,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
134146

135147
try {
136148
$start = microtime(true);
137-
$this->getFileMoveService()->cloudinaryUploadFile($fileObject, $this->targetStorage, $input->getOption('base-url'));
149+
$this->getFileMoveService()->cloudinaryUploadFile($fileObject, $target, $newIdentifier, $input->getOption('base-url'));
138150
$timeElapsedSeconds = microtime(true) - $start;
139151
$this->log('File uploaded, Elapsed time %.3f', [$timeElapsedSeconds]);
140152
} catch (Exception $e) {
@@ -148,9 +160,25 @@ protected function execute(InputInterface $input, OutputInterface $output): int
148160
}
149161
}
150162

151-
// changing file storage and hard delete the file from the current storage
163+
// Update sys_file entry
164+
// See \TYPO3\CMS\Core\Resource\ResourceStorage::moveFile for reference
152165
$this->log('Changing storage for file %s', [$fileObject->getIdentifier()]);
153-
$this->getFileMoveService()->changeStorage($fileObject, $this->targetStorage);
166+
$oldIdentifier = $fileObject->getIdentifier();
167+
$oldFolder = $fileObject->getParentFolder();
168+
$fileObject->updateProperties(['storage' => $target->getStorage()->getUid(), 'identifier' => $newIdentifier]);
169+
$newFolder = $fileObject->getParentFolder();
170+
// clean up processed files
171+
$this->eventDispatcher->dispatch(new AfterFileAddedEvent($fileObject, $newFolder));
172+
$targetIndexer->updateIndexEntry($fileObject);
173+
174+
// Delete the file from the source storage without deleting the sys_file record
175+
if ($sourceFileExists) {
176+
$sourceStorageDriver->deleteFile($oldIdentifier);
177+
$sourceFileExists = false;
178+
}
179+
180+
$this->eventDispatcher->dispatch(new AfterFileMovedEvent($fileObject, $newFolder, $oldFolder));
181+
154182
$counter++;
155183
}
156184
$this->log(LF);
@@ -208,4 +236,12 @@ protected function getFileMoveService(): FileMoveService
208236
{
209237
return GeneralUtility::makeInstance(FileMoveService::class);
210238
}
239+
240+
protected function getStorageDriver(ResourceStorage $storage): DriverInterface
241+
{
242+
$reflection = new \ReflectionClass($storage);
243+
$property = $reflection->getProperty('driver');
244+
$property->setAccessible(true);
245+
return $property->getValue($storage);
246+
}
211247
}

Classes/Services/FileMoveService.php

Lines changed: 19 additions & 47 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,17 +28,19 @@ 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);
39+
$resource = $this->getAdminApi($storage)->asset($publicId);
4040
$fileExists = !empty($resource);
41+
if ($fileExists) {
42+
(new CloudinaryResourceService($storage))->save((array)$resource);
43+
}
4144
} catch (Exception $exception) {
4245
$fileExists = false;
4346
}
@@ -74,34 +77,6 @@ public function fileExists(File $fileObject, ResourceStorage $targetStorage): bo
7477
# return $isUpdated && $isDeletedFromSourceStorage;
7578
#}
7679

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-
10580
protected function getAbsolutePath(File $fileObject): string
10681
{
10782
// Compute the absolute file name of the file to move
@@ -112,37 +87,34 @@ protected function getAbsolutePath(File $fileObject): string
11287

11388
public function cloudinaryUploadFile(
11489
File $fileObject,
115-
ResourceStorage $targetStorage,
90+
Folder $targetFolder,
91+
string $newIdentifier,
11692
string $baseUrl = ''
11793
): void {
118-
94+
$targetStorage = $targetFolder->getStorage();
11995
$this->initializeCloudinaryService($targetStorage);
120-
121-
$this->ensureDirectoryExistence($fileObject);
122-
123-
124-
$fileIdentifier = $fileObject->getIdentifier();
125-
$publicId = $this->getCloudinaryPathService()
126-
->computeCloudinaryPublicId($fileIdentifier);
96+
$publicId = $this->getCloudinaryPathService()->computeCloudinaryPublicId($newIdentifier);
12797

12898
$options = [
12999
'public_id' => basename($publicId),
130100
'folder' => $this->getCloudinaryPathService()
131101
->computeCloudinaryFolderPath(
132-
$fileObject->getParentFolder()->getIdentifier()
102+
dirname($newIdentifier)
133103
),
134-
'resource_type' => $this->getCloudinaryPathService()->getResourceType($fileIdentifier),
104+
'resource_type' => $this->getCloudinaryPathService()->getResourceType($newIdentifier),
135105
'overwrite' => true,
136106
];
137107
$fileNameAndPath = $baseUrl
138-
? rtrim($baseUrl, DIRECTORY_SEPARATOR) . $fileIdentifier
139-
: $this->getAbsolutePath($fileObject);
108+
? rtrim($baseUrl, DIRECTORY_SEPARATOR) . $fileObject->getIdentifier()
109+
: ($fileObject->getPublicUrl() ?: $fileObject->getForLocalProcessing(false));
140110

141111
// Upload the file
142-
$this->getUploadApi($targetStorage)->upload(
112+
$response = $this->getUploadApi($targetStorage)->upload(
143113
$fileNameAndPath,
144114
$options
145115
);
116+
$resource = (array)$response;
117+
(new CloudinaryResourceService($targetStorage))->save($resource);
146118
}
147119

148120
protected function getQueryBuilder(): QueryBuilder

0 commit comments

Comments
 (0)