From fab21d38386f92349a8dc9aeb953219953d14945 Mon Sep 17 00:00:00 2001 From: Helmut Hummel <helmut.hummel@typo3.org> Date: Mon, 22 Sep 2014 15:32:52 +0200 Subject: [PATCH] [BUGFIX] Allow processed folders in different storage The processingfolder of a storage can now be a combined identifier. This makes it possible to have the processed files outside of the storage in case of a read-only storage for instance. Releases: master, 6.2 Resolves: #61463 Change-Id: I4f0e187db2aede33be40f62df3bb9f63e9706d46 Reviewed-on: http://review.typo3.org/32921 Reviewed-by: Nicole Cordes <typo3@cordes.co> Tested-by: Nicole Cordes <typo3@cordes.co> Reviewed-by: Markus Klein <klein.t3@reelworx.at> Tested-by: Markus Klein <klein.t3@reelworx.at> --- .../locallang_csh_sysfilestorage.xlf | 2 +- .../core/Classes/Resource/Index/Indexer.php | 2 +- .../core/Classes/Resource/ProcessedFile.php | 8 +- .../core/Classes/Resource/ResourceStorage.php | 103 ++++++++++++++---- .../Service/FileProcessingService.php | 6 - .../Classes/Resource/StorageRepository.php | 6 +- .../Configuration/TCA/sys_file_storage.php | 2 +- ...llowProcessedFoldersInDifferentStorage.rst | 18 +++ .../Tests/Unit/Resource/ProcessedFileTest.php | 11 ++ .../Unit/Resource/ResourceStorageTest.php | 4 +- .../Classes/Controller/FileListController.php | 3 +- typo3/sysext/lang/locallang_tca.xlf | 3 + 12 files changed, 126 insertions(+), 42 deletions(-) create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-61463-AllowProcessedFoldersInDifferentStorage.rst diff --git a/typo3/sysext/context_help/locallang_csh_sysfilestorage.xlf b/typo3/sysext/context_help/locallang_csh_sysfilestorage.xlf index b36ba4018425..ec866e450937 100644 --- a/typo3/sysext/context_help/locallang_csh_sysfilestorage.xlf +++ b/typo3/sysext/context_help/locallang_csh_sysfilestorage.xlf @@ -34,7 +34,7 @@ <source>The driver is the technical background of a storage, and thus also defines whether the location of the storage is local (= on the same machine as this TYPO3 installation) or remote like a WebDAV server connection. Search for extensions in the TYPO3 Extension Repository that extend this driver list.</source> </trans-unit> <trans-unit id="processingfolder.description" xml:space="preserve"> - <source>Defines the directory on this storage to be used to save temporary files like resized images.</source> + <source>Defines the directory on this storage to be used to save temporary files like resized images. When on a different storage make sure that the folder exists and is writable.</source> </trans-unit> <trans-unit id="is_browsable.description" xml:space="preserve"> <source>If set, backend users can browse through this storage. Otherwise it will show up as a unselectable item in the list.</source> diff --git a/typo3/sysext/core/Classes/Resource/Index/Indexer.php b/typo3/sysext/core/Classes/Resource/Index/Indexer.php index e5dce8942a78..e122a6e99873 100644 --- a/typo3/sysext/core/Classes/Resource/Index/Indexer.php +++ b/typo3/sysext/core/Classes/Resource/Index/Indexer.php @@ -143,7 +143,7 @@ class Indexer { protected function detectChangedFilesInStorage(array $fileIdentifierArray) { foreach ($fileIdentifierArray as $fileIdentifier) { // skip processed files - if (strpos($fileIdentifier, $this->storage->getProcessingFolder()->getIdentifier()) === 0) { + if ($this->storage->isWithinProcessingFolder($fileIdentifier)) { continue; } // Get the modification time for file-identifier from the storage diff --git a/typo3/sysext/core/Classes/Resource/ProcessedFile.php b/typo3/sysext/core/Classes/Resource/ProcessedFile.php index fe2619f7e2b3..0eaee85748ad 100644 --- a/typo3/sysext/core/Classes/Resource/ProcessedFile.php +++ b/typo3/sysext/core/Classes/Resource/ProcessedFile.php @@ -113,7 +113,7 @@ class ProcessedFile extends AbstractFile { */ public function __construct(File $originalFile, $taskType, array $processingConfiguration, array $databaseRow = NULL) { $this->originalFile = $originalFile; - $this->storage = $originalFile->getStorage(); + $this->storage = $originalFile->getStorage()->getProcessingFolder()->getStorage(); $this->taskType = $taskType; $this->processingConfiguration = $processingConfiguration; if (is_array($databaseRow)) { @@ -177,8 +177,8 @@ class ProcessedFile extends AbstractFile { if ($this->identifier === NULL) { throw new \RuntimeException('Cannot update original file!', 1350582054); } - // @todo this should be more generic (in fact it only works for local file paths) - $addedFile = $this->storage->updateProcessedFile($filePath, $this); + $processingFolder = $this->originalFile->getStorage()->getProcessingFolder(); + $addedFile = $this->storage->updateProcessedFile($filePath, $this, $processingFolder); // Update some related properties $this->identifier = $addedFile->getIdentifier(); @@ -454,7 +454,7 @@ class ProcessedFile extends AbstractFile { } // hash does not match - if (array_key_exists('checksum', $this->properties) && $this->calculateChecksum() !== $this->properties['checksum']) { + if (array_key_exists('checksum', $this->properties) && $this->calculateChecksum() !== $this->properties['checksum']) { $fileMustBeRecreated = TRUE; } diff --git a/typo3/sysext/core/Classes/Resource/ResourceStorage.php b/typo3/sysext/core/Classes/Resource/ResourceStorage.php index 9872fd72fb61..b09cb90e8395 100644 --- a/typo3/sysext/core/Classes/Resource/ResourceStorage.php +++ b/typo3/sysext/core/Classes/Resource/ResourceStorage.php @@ -14,7 +14,6 @@ namespace TYPO3\CMS\Core\Resource; * The TYPO3 project - inspiring people to share! */ -use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderWritePermissionsException; use TYPO3\CMS\Core\Resource\Exception\InvalidTargetFolderException; use TYPO3\CMS\Core\Resource\Index\FileIndexRepository; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -1071,17 +1070,22 @@ class ResourceStorage implements ResourceStorageInterface { /** * Updates a processed file with a new file from the local filesystem. * - * @param $localFilePath + * @param string $localFilePath * @param ProcessedFile $processedFile + * @param Folder $processingFolder * @return FileInterface * @throws \InvalidArgumentException * @internal use only */ - public function updateProcessedFile($localFilePath, ProcessedFile $processedFile) { + public function updateProcessedFile($localFilePath, ProcessedFile $processedFile, Folder $processingFolder = NULL) { if (!file_exists($localFilePath)) { throw new \InvalidArgumentException('File "' . $localFilePath . '" does not exist.', 1319552746); } - $fileIdentifier = $this->driver->addFile($localFilePath, $this->getProcessingFolder()->getIdentifier(), $processedFile->getName()); + if ($processingFolder === NULL) { + $processingFolder = $this->getProcessingFolder(); + } + $fileIdentifier = $this->driver->addFile($localFilePath, $processingFolder->getIdentifier(), $processedFile->getName()); + // @todo check if we have to update the processed file other then the identifier $processedFile->setIdentifier($fileIdentifier); return $processedFile; @@ -1213,7 +1217,7 @@ class ResourceStorage implements ResourceStorageInterface { * @return FileInterface */ public function getFile($identifier) { - $file = $this->getFileFactory()->getFileObjectByStorageAndIdentifier($this->getUid(), $identifier); + $file = $this->getFileFactory()->getFileObjectByStorageAndIdentifier($this->getUid(), $identifier); if (!$this->driver->fileExists($identifier)) { $file->setMissing(TRUE); } @@ -1360,23 +1364,59 @@ class ResourceStorage implements ResourceStorageInterface { return $this->driver->getFoldersInFolder($folderIdentifier, 0, 0, $recursive, $filters); } - /** - * Returns TRUE if the specified file exists. + * Returns TRUE if the specified file exists * * @param string $identifier * @return bool */ public function hasFile($identifier) { // Allow if identifier is in processing folder - if (!$this->driver->isWithin($this->getProcessingFolder()->getIdentifier(), $identifier)) { + if (!$this->isWithinProcessingFolder($identifier)) { $this->assureFolderReadPermission(); } return $this->driver->fileExists($identifier); } /** - * Checks if the queried file in the given folder exists. + * Get all processing folders that live in this storage + * + * @return Folder[] + */ + public function getProcessingFolders() { + $processingFolders = array(); + + /** @var $storageRepository StorageRepository */ + $storageRepository = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\StorageRepository::class); + $allStorages = $storageRepository->findAll(); + foreach ($allStorages as $storage) { + if ($storage->getProcessingFolder()->getStorage() === $this) { + $processingFolders[] = $storage->getProcessingFolder(); + } + } + return $processingFolders; + } + + /** + * Returns TRUE if folder that is in current storage is set as + * processing folder for one of the existing storages + * + * @param Folder $folder + * @return bool + */ + public function isProcessingFolder(Folder $folder) { + $isProcessingFolder = FALSE; + foreach ($this->getProcessingFolders() as $processingFolder) { + if ($folder->getCombinedIdentifier() === $processingFolder->getCombinedIdentifier()) { + $isProcessingFolder = TRUE; + break; + } + } + return $isProcessingFolder; + } + + /** + * Checks if the queried file in the given folder exists * * @param string $fileName * @param Folder $folder @@ -1704,10 +1744,12 @@ class ResourceStorage implements ResourceStorageInterface { foreach ($folder->getSubfolders() as $subfolder) { $folderQueue[] = $subfolder; } - foreach ($folder->getFiles() as $file) { /** @var FileInterface $file */ + foreach ($folder->getFiles() as $file) { + /** @var FileInterface $file */ $files[$file->getIdentifier()] = $file; } } + return $files; } @@ -1718,7 +1760,7 @@ class ResourceStorage implements ResourceStorageInterface { * @param Folder $folderToMove The folder to move. * @param Folder $targetParentFolder The target parent folder * @param string $newFolderName - * @param string $conflictMode How to handle conflicts; one of "overrideExistingFile", "renameNewFolder", "cancel + * @param string $conflictMode How to handle conflicts; one of "overrideExistingFile", "renameNewFolder", "cancel * * @throws \Exception|\TYPO3\CMS\Core\Exception * @throws \InvalidArgumentException @@ -1782,7 +1824,7 @@ class ResourceStorage implements ResourceStorageInterface { * @param FolderInterface $folderToCopy The folder to copy * @param FolderInterface $targetParentFolder The target folder * @param string $newFolderName - * @param string $conflictMode "overrideExistingFolder", "renameNewFolder", "cancel + * @param string $conflictMode "overrideExistingFolder", "renameNewFolder", "cancel * @return Folder The new (copied) folder object * @throws InvalidTargetFolderException */ @@ -1912,9 +1954,12 @@ class ResourceStorage implements ResourceStorageInterface { $filters = $useFilters == TRUE ? $this->fileAndFolderNameFilters : array(); $folderIdentifiers = $this->driver->getFoldersInFolder($folder->getIdentifier(), $start, $maxNumberOfItems, $recursive, $filters); - $processingIdentifier = $this->getProcessingFolder()->getIdentifier(); - if (isset($folderIdentifiers[$processingIdentifier])) { - unset($folderIdentifiers[$processingIdentifier]); + // Exclude processing folders + foreach ($this->getProcessingFolders() as $processingFolder) { + $processingIdentifier = $processingFolder->getIdentifier(); + if (isset($folderIdentifiers[$processingIdentifier])) { + unset($folderIdentifiers[$processingIdentifier]); + } } $folders = array(); foreach ($folderIdentifiers as $folderIdentifier) { @@ -2021,11 +2066,20 @@ class ResourceStorage implements ResourceStorageInterface { } /** + * Returns TRUE if the specified file is in a folder that is set a processing for a storage + * * @param string $identifier * @return bool */ public function isWithinProcessingFolder($identifier) { - return $this->driver->isWithin($this->getProcessingFolder()->getIdentifier(), $identifier); + $inProcessingFolder = FALSE; + foreach ($this->getProcessingFolders() as $processingFolder) { + if ($this->driver->isWithin($processingFolder->getIdentifier(), $identifier)) { + $inProcessingFolder = TRUE; + break; + } + } + return $inProcessingFolder; } /** @@ -2458,8 +2512,7 @@ class ResourceStorage implements ResourceStorageInterface { $folderRole = FolderInterface::ROLE_USER_MOUNT; } } - - if ($identifier === $this->getProcessingFolder()->getIdentifier()) { + if ($folder instanceof Folder && $this->isProcessingFolder($folder)) { $folderRole = FolderInterface::ROLE_PROCESSING; } @@ -2479,13 +2532,17 @@ class ResourceStorage implements ResourceStorageInterface { $processingFolder = $this->storageRecord['processingfolder']; } try { - if ($this->driver->folderExists($processingFolder) === FALSE) { - $this->processingFolder = $this->createFolder($processingFolder); + if (strpos($processingFolder, ':') !== FALSE) { + $this->processingFolder = ResourceFactory::getInstance()->getFolderObjectFromCombinedIdentifier($processingFolder); } else { - $data = $this->driver->getFolderInfoByIdentifier($processingFolder); - $this->processingFolder = ResourceFactory::getInstance()->createFolderObject($this, $data['identifier'], $data['name']); + if ($this->driver->folderExists($processingFolder) === FALSE) { + $this->processingFolder = $this->createFolder($processingFolder); + } else { + $data = $this->driver->getFolderInfoByIdentifier($processingFolder); + $this->processingFolder = ResourceFactory::getInstance()->createFolderObject($this, $data['identifier'], $data['name']); + } } - } catch(InsufficientFolderWritePermissionsException $e) { + } catch(Exception\InsufficientFolderWritePermissionsException $e) { $this->processingFolder = GeneralUtility::makeInstance( InaccessibleFolder::class, $this, $processingFolder, $processingFolder ); diff --git a/typo3/sysext/core/Classes/Resource/Service/FileProcessingService.php b/typo3/sysext/core/Classes/Resource/Service/FileProcessingService.php index 1313452cce7a..d1db1cd03fd8 100644 --- a/typo3/sysext/core/Classes/Resource/Service/FileProcessingService.php +++ b/typo3/sysext/core/Classes/Resource/Service/FileProcessingService.php @@ -101,14 +101,8 @@ class FileProcessingService { * * @param Resource\ProcessedFile $processedFile * @param Resource\ResourceStorage $targetStorage The storage to put the processed file into - * - * @throws \RuntimeException */ protected function process(Resource\ProcessedFile $processedFile, Resource\ResourceStorage $targetStorage) { - $targetFolder = $targetStorage->getProcessingFolder(); - if (!is_object($targetFolder)) { - throw new \RuntimeException('Could not get processing folder for storage ' . $this->storage->getName(), 1350514301); - } // We only have to trigger the file processing if the file either is new, does not exist or the // original file has changed since the last processing run (the last case has to trigger a reprocessing diff --git a/typo3/sysext/core/Classes/Resource/StorageRepository.php b/typo3/sysext/core/Classes/Resource/StorageRepository.php index 186dd653c696..f951932098b7 100644 --- a/typo3/sysext/core/Classes/Resource/StorageRepository.php +++ b/typo3/sysext/core/Classes/Resource/StorageRepository.php @@ -23,7 +23,7 @@ namespace TYPO3\CMS\Core\Resource; class StorageRepository extends AbstractRepository { /** - * @var null|array‚ + * @var NULL|array‚ */ protected static $storageRowCache = NULL; @@ -69,12 +69,12 @@ class StorageRepository extends AbstractRepository { /** * @param int $uid * - * @return null|ResourceStorage + * @return NULL|ResourceStorage */ public function findByUid($uid) { $this->initializeLocalCache(); if (isset(self::$storageRowCache[$uid])) { - return $this->factory->getStorageObject($uid, self::$storageRowCache[$uid]); + return $this->factory->getStorageObject($uid, self::$storageRowCache[$uid]); } return NULL; } diff --git a/typo3/sysext/core/Configuration/TCA/sys_file_storage.php b/typo3/sysext/core/Configuration/TCA/sys_file_storage.php index 40c6f8395094..813eed729159 100644 --- a/typo3/sysext/core/Configuration/TCA/sys_file_storage.php +++ b/typo3/sysext/core/Configuration/TCA/sys_file_storage.php @@ -87,7 +87,7 @@ return array( 'label' => 'LLL:EXT:lang/locallang_tca.xlf:sys_file_storage.processingfolder', 'config' => array( 'type' => 'input', - 'placeholder' => \TYPO3\CMS\Core\Resource\ResourceStorage::DEFAULT_ProcessingFolder, + 'placeholder' => 'LLL:EXT:lang/locallang_tca.xlf:sys_file_storage.processingfolder.placeholder', 'size' => '20' ) ), diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-61463-AllowProcessedFoldersInDifferentStorage.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-61463-AllowProcessedFoldersInDifferentStorage.rst new file mode 100644 index 000000000000..a558ea6c0286 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-61463-AllowProcessedFoldersInDifferentStorage.rst @@ -0,0 +1,18 @@ +============================================================== +Feature: #61463 - Allow processed folders in different storage +============================================================== + +Description +=========== + +The processing folder of a storage can now be a combined identifier. +This makes it possible to have the processed files outside of the +storage in case of a read-only storage for instance. + + +Impact +====== + +For existing systems there is no impact. When the processing folder is changed +to a folder in a different storage you need to make sure the folder exists +and is writable. \ No newline at end of file diff --git a/typo3/sysext/core/Tests/Unit/Resource/ProcessedFileTest.php b/typo3/sysext/core/Tests/Unit/Resource/ProcessedFileTest.php index ac4dbf1a666e..6a5ee2f44100 100644 --- a/typo3/sysext/core/Tests/Unit/Resource/ProcessedFileTest.php +++ b/typo3/sysext/core/Tests/Unit/Resource/ProcessedFileTest.php @@ -17,12 +17,18 @@ namespace TYPO3\CMS\Core\Tests\Unit\Resource; use TYPO3\CMS\Core\Resource\ProcessedFile; use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Resource\File; +use TYPO3\CMS\Core\Resource\Folder; /** * Testcase for the ProcessedFile class of the TYPO3 FAL */ class ProcessedFileTest extends \TYPO3\CMS\Core\Tests\UnitTestCase { + /** + * @var \PHPUnit_Framework_MockObject_MockObject|Folder + */ + protected $folderMock; + /** * @var \PHPUnit_Framework_MockObject_MockObject|ResourceStorage */ @@ -40,6 +46,11 @@ class ProcessedFileTest extends \TYPO3\CMS\Core\Tests\UnitTestCase { $this->storageMock = $this->getMock(ResourceStorage::class, array(), array(), '', FALSE); $this->storageMock->expects($this->any())->method('getUid')->will($this->returnValue(5)); + $this->folderMock = $this->getMock(Folder::class, array(), array(), '', FALSE); + $this->folderMock->expects($this->any())->method('getStorage')->willReturn($this->storageMock); + + $this->storageMock->expects($this->any())->method('getProcessingFolder')->willReturn($this->folderMock); + $this->databaseRow = array( 'uid' => '1234567', 'identifier' => 'dummy.txt', diff --git a/typo3/sysext/core/Tests/Unit/Resource/ResourceStorageTest.php b/typo3/sysext/core/Tests/Unit/Resource/ResourceStorageTest.php index f82e49cbc420..78d24e281a74 100644 --- a/typo3/sysext/core/Tests/Unit/Resource/ResourceStorageTest.php +++ b/typo3/sysext/core/Tests/Unit/Resource/ResourceStorageTest.php @@ -53,7 +53,9 @@ class ResourceStorageTest extends BaseTestCase { FileRepository::class, $fileRepositoryMock ); - $GLOBALS['TYPO3_DB'] = $this->getMock(DatabaseConnection::class); + $databaseMock = $this->getMock(DatabaseConnection::class); + $databaseMock->expects($this->any())->method('exec_SELECTgetRows')->with('*', 'sys_file_storage', '1=1', '', 'name', '', 'uid')->willReturn(array()); + $GLOBALS['TYPO3_DB'] = $databaseMock; } protected function tearDown() { diff --git a/typo3/sysext/filelist/Classes/Controller/FileListController.php b/typo3/sysext/filelist/Classes/Controller/FileListController.php index 6c1496383e93..c9654c6488df 100644 --- a/typo3/sysext/filelist/Classes/Controller/FileListController.php +++ b/typo3/sysext/filelist/Classes/Controller/FileListController.php @@ -165,8 +165,7 @@ class FileListController { $this->folderObject = $fileFactory->getFolderObjectFromCombinedIdentifier($storage->getUid() . ':' . $identifier); // Disallow the rendering of the processing folder (e.g. could be called manually) // and all folders without any defined storage - if ($this->folderObject && ($this->folderObject->getStorage()->getUid() == 0 || trim($this->folderObject->getStorage()->getProcessingFolder()->getIdentifier(), '/') === trim($this->folderObject->getIdentifier(), '/'))) { - $storage = $fileFactory->getStorageObjectFromCombinedIdentifier($combinedIdentifier); + if ($this->folderObject && ($storage->getUid() === 0 || $storage->isProcessingFolder($this->folderObject))) { $this->folderObject = $storage->getRootLevelFolder(); } } else { diff --git a/typo3/sysext/lang/locallang_tca.xlf b/typo3/sysext/lang/locallang_tca.xlf index d71c6d1283d6..2c03d6db4830 100644 --- a/typo3/sysext/lang/locallang_tca.xlf +++ b/typo3/sysext/lang/locallang_tca.xlf @@ -426,6 +426,9 @@ <trans-unit id="sys_file_storage.processingfolder" xml:space="preserve"> <source>Folder for manipulated and temporary images etc.</source> </trans-unit> + <trans-unit id="sys_file_storage.processingfolder.placeholder" xml:space="preserve"> + <source>_processed_ or 1:/processed_files_storage_x</source> + </trans-unit> <trans-unit id="sys_file_storage.driver" xml:space="preserve"> <source>Driver</source> </trans-unit> -- GitLab