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