1010 */
1111
1212use Exception ;
13+ use Psr \EventDispatcher \EventDispatcherInterface ;
1314use Symfony \Component \Console \Command \Command ;
1415use 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 ;
1521use TYPO3 \CMS \Core \Resource \ResourceStorage ;
1622use TYPO3 \CMS \Core \Utility \PathUtility ;
1723use 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}
0 commit comments