[BUGFIX] Allow processed folders in different storage 21/32921/11
authorHelmut Hummel <helmut.hummel@typo3.org>
Mon, 22 Sep 2014 13:32:52 +0000 (15:32 +0200)
committerMarkus Klein <klein.t3@reelworx.at>
Fri, 6 Mar 2015 09:05:42 +0000 (10:05 +0100)
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>
12 files changed:
typo3/sysext/context_help/locallang_csh_sysfilestorage.xlf
typo3/sysext/core/Classes/Resource/Index/Indexer.php
typo3/sysext/core/Classes/Resource/ProcessedFile.php
typo3/sysext/core/Classes/Resource/ResourceStorage.php
typo3/sysext/core/Classes/Resource/Service/FileProcessingService.php
typo3/sysext/core/Classes/Resource/StorageRepository.php
typo3/sysext/core/Configuration/TCA/sys_file_storage.php
typo3/sysext/core/Documentation/Changelog/master/Feature-61463-AllowProcessedFoldersInDifferentStorage.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Resource/ProcessedFileTest.php
typo3/sysext/core/Tests/Unit/Resource/ResourceStorageTest.php
typo3/sysext/filelist/Classes/Controller/FileListController.php
typo3/sysext/lang/locallang_tca.xlf

index b36ba40..ec866e4 100644 (file)
@@ -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>
index e5dce89..e122a6e 100644 (file)
@@ -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
index fe2619f..0eaee85 100644 (file)
@@ -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;
                }
 
index 9872fd7..b09cb90 100644 (file)
@@ -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
                                );
index 1313452..d1db1cd 100644 (file)
@@ -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
index 186dd65..f951932 100644 (file)
@@ -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;
        }
index 40c6f83..813eed7 100644 (file)
@@ -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 (file)
index 0000000..a558ea6
--- /dev/null
@@ -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
index ac4dbf1..6a5ee2f 100644 (file)
@@ -17,6 +17,7 @@ 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
@@ -24,6 +25,11 @@ use TYPO3\CMS\Core\Resource\File;
 class ProcessedFileTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
 
        /**
+        * @var \PHPUnit_Framework_MockObject_MockObject|Folder
+        */
+       protected $folderMock;
+
+       /**
         * @var \PHPUnit_Framework_MockObject_MockObject|ResourceStorage
         */
        protected $storageMock;
@@ -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',
index f82e49c..78d24e2 100644 (file)
@@ -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() {
index 6c14963..c9654c6 100644 (file)
@@ -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 {
index d71c6d1..2c03d6d 100644 (file)
                        <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>