2 namespace TYPO3\CMS\Core\
Resource\Driver
;
5 * This file is part of the TYPO3 CMS project.
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
14 * The TYPO3 project - inspiring people to share!
17 use TYPO3\CMS\Core\
Resource\FolderInterface
;
18 use TYPO3\CMS\Core\
Resource\ResourceStorage
;
19 use TYPO3\CMS\Core\Type\File\FileInfo
;
20 use TYPO3\CMS\Core\Utility\GeneralUtility
;
21 use TYPO3\CMS\Core\Utility\PathUtility
;
22 use TYPO3\CMS\Core\
Resource\Exception
;
25 * Driver for the local file system
28 class LocalDriver
extends AbstractHierarchicalFilesystemDriver
{
33 const UNSAFE_FILENAME_CHARACTER_EXPRESSION
= '\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF';
36 * The absolute base path. It always contains a trailing slash.
40 protected $absoluteBasePath;
43 * A list of all supported hash algorithms, written all lower case.
47 protected $supportedHashAlgorithms = array('sha1', 'md5');
50 * The base URL that points to this driver's storage. As long is this
51 * is not set, it is assumed that this folder is not publicly available
55 protected $baseUri = NULL;
58 * @var \TYPO3\CMS\Core\Charset\CharsetConverter
60 protected $charsetConversion;
63 protected $mappingFolderNameToRole = array(
64 '_recycler_' => FolderInterface
::ROLE_RECYCLER
,
65 '_temp_' => FolderInterface
::ROLE_TEMPORARY
,
66 'user_upload' => FolderInterface
::ROLE_USERUPLOAD
,
70 * @param array $configuration
72 public function __construct(array $configuration = array()) {
73 parent
::__construct($configuration);
74 // The capabilities default of this driver. See CAPABILITY_* constants for possible values
76 ResourceStorage
::CAPABILITY_BROWSABLE
77 | ResourceStorage
::CAPABILITY_PUBLIC
78 | ResourceStorage
::CAPABILITY_WRITABLE
;
82 * Merges the capabilites merged by the user at the storage
83 * configuration into the actual capabilities of the driver
84 * and returns the result.
86 * @param int $capabilities
89 public function mergeConfigurationCapabilities($capabilities) {
90 $this->capabilities
&= $capabilities;
91 return $this->capabilities
;
96 * Processes the configuration for this driver.
100 public function processConfiguration() {
101 $this->absoluteBasePath
= $this->calculateBasePath($this->configuration
);
102 $this->determineBaseUrl();
103 if ($this->baseUri
=== NULL) {
104 // remove public flag
105 $this->capabilities
&= ~ResourceStorage
::CAPABILITY_PUBLIC
;
110 * Initializes this object. This is called by the storage after the driver
115 public function initialize() {
119 * Determines the base URL for this driver, from the configuration or
120 * the TypoScript frontend object
124 protected function determineBaseUrl() {
125 // only calculate baseURI if the storage does not enforce jumpUrl Script
126 if ($this->hasCapability(ResourceStorage
::CAPABILITY_PUBLIC
)) {
127 if (GeneralUtility
::isFirstPartOfStr($this->absoluteBasePath
, PATH_site
)) {
128 // use site-relative URLs
129 $temporaryBaseUri = rtrim(PathUtility
::stripPathSitePrefix($this->absoluteBasePath
), '/');
130 if ($temporaryBaseUri !== '') {
131 $uriParts = explode('/', $temporaryBaseUri);
132 $uriParts = array_map('rawurlencode', $uriParts);
133 $temporaryBaseUri = implode('/', $uriParts) . '/';
135 $this->baseUri
= $temporaryBaseUri;
136 } elseif (isset($this->configuration
['baseUri']) && GeneralUtility
::isValidUrl($this->configuration
['baseUri'])) {
137 $this->baseUri
= rtrim($this->configuration
['baseUri'], '/') . '/';
143 * Calculates the absolute path to this drivers storage location.
145 * @throws Exception\InvalidConfigurationException
146 * @param array $configuration
149 protected function calculateBasePath(array $configuration) {
150 if (!array_key_exists('basePath', $configuration) ||
empty($configuration['basePath'])) {
151 throw new Exception\
InvalidConfigurationException(
152 'Configuration must contain base path.',
157 if ($configuration['pathType'] === 'relative') {
158 $relativeBasePath = $configuration['basePath'];
159 $absoluteBasePath = PATH_site
. $relativeBasePath;
161 $absoluteBasePath = $configuration['basePath'];
163 $absoluteBasePath = $this->canonicalizeAndCheckFilePath($absoluteBasePath);
164 $absoluteBasePath = rtrim($absoluteBasePath, '/') . '/';
165 if (!is_dir($absoluteBasePath)) {
166 throw new Exception\
InvalidConfigurationException(
167 'Base path "' . $absoluteBasePath . '" does not exist or is no directory.',
171 return $absoluteBasePath;
175 * Returns the public URL to a file.
176 * For the local driver, this will always return a path relative to PATH_site.
178 * @param string $identifier
180 * @throws \TYPO3\CMS\Core\Resource\Exception
182 public function getPublicUrl($identifier) {
184 if ($this->baseUri
!== NULL) {
185 $uriParts = explode('/', ltrim($identifier, '/'));
186 $uriParts = array_map('rawurlencode', $uriParts);
187 $identifier = implode('/', $uriParts);
188 $publicUrl = $this->baseUri
. $identifier;
194 * Returns the Identifier of the root level folder of the storage.
198 public function getRootLevelFolder() {
203 * Returns identifier of the default folder new files should be put into.
207 public function getDefaultFolder() {
208 $identifier = '/user_upload/';
209 $createFolder = !$this->folderExists($identifier);
210 if ($createFolder === TRUE) {
211 $identifier = $this->createFolder('user_upload');
217 * Creates a folder, within a parent folder.
218 * If no parent folder is given, a rootlevel folder will be created
220 * @param string $newFolderName
221 * @param string $parentFolderIdentifier
222 * @param bool $recursive
223 * @return string the Identifier of the new folder
225 public function createFolder($newFolderName, $parentFolderIdentifier = '', $recursive = FALSE) {
226 $parentFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier);
227 $newFolderName = trim($newFolderName, '/');
228 if ($recursive == FALSE) {
229 $newFolderName = $this->sanitizeFileName($newFolderName);
230 $newIdentifier = $parentFolderIdentifier . $newFolderName . '/';
231 GeneralUtility
::mkdir($this->getAbsolutePath($newIdentifier));
233 $parts = GeneralUtility
::trimExplode('/', $newFolderName);
234 $parts = array_map(array($this, 'sanitizeFileName'), $parts);
235 $newFolderName = implode('/', $parts);
236 $newIdentifier = $parentFolderIdentifier . $newFolderName . '/';
237 GeneralUtility
::mkdir_deep($this->getAbsolutePath($parentFolderIdentifier) . '/', $newFolderName);
239 return $newIdentifier;
243 * Returns information about a file.
245 * @param string $fileIdentifier In the case of the LocalDriver, this is the (relative) path to the file.
246 * @param array $propertiesToExtract Array of properties which should be extracted, if empty all will be extracted
248 * @throws \InvalidArgumentException
250 public function getFileInfoByIdentifier($fileIdentifier, array $propertiesToExtract = array()) {
251 $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
252 // don't use $this->fileExists() because we need the absolute path to the file anyways, so we can directly
253 // use PHP's filesystem method.
254 if (!file_exists($absoluteFilePath) ||
!is_file($absoluteFilePath)) {
255 throw new \
InvalidArgumentException('File ' . $fileIdentifier . ' does not exist.', 1314516809);
258 $dirPath = PathUtility
::dirname($fileIdentifier);
259 $dirPath = $this->canonicalizeAndCheckFolderIdentifier($dirPath);
260 return $this->extractFileInformation($absoluteFilePath, $dirPath, $propertiesToExtract);
264 * Returns information about a folder.
266 * @param string $folderIdentifier In the case of the LocalDriver, this is the (relative) path to the file.
268 * @throws Exception\FolderDoesNotExistException
270 public function getFolderInfoByIdentifier($folderIdentifier) {
271 $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
273 if (!$this->folderExists($folderIdentifier)) {
274 throw new Exception\
FolderDoesNotExistException(
275 'Folder "' . $folderIdentifier . '" does not exist.',
280 'identifier' => $folderIdentifier,
281 'name' => PathUtility
::basename($folderIdentifier),
282 'storage' => $this->storageUid
287 * Returns a string where any character not matching [.a-zA-Z0-9_-] is
289 * Trailing dots are removed
291 * Previously in \TYPO3\CMS\Core\Utility\File\BasicFileUtility::cleanFileName()
293 * @param string $fileName Input string, typically the body of a fileName
294 * @param string $charset Charset of the a fileName (defaults to current charset; depending on context)
295 * @return string Output string with any characters not matching [.a-zA-Z0-9_-] is substituted by '_' and trailing dots removed
296 * @throws Exception\InvalidFileNameException
298 public function sanitizeFileName($fileName, $charset = '') {
299 // Handle UTF-8 characters
300 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']) {
301 // Allow ".", "-", 0-9, a-z, A-Z and everything beyond U+C0 (latin capital letter a with grave)
302 $cleanFileName = preg_replace('/[' . self
::UNSAFE_FILENAME_CHARACTER_EXPRESSION
. ']/u', '_', trim($fileName));
304 // Define character set
306 if (TYPO3_MODE
=== 'FE') {
307 $charset = $GLOBALS['TSFE']->renderCharset
;
309 // default for Backend
313 // If a charset was found, convert fileName
315 $fileName = $this->getCharsetConversion()->specCharsToASCII($charset, $fileName);
317 // Replace unwanted characters by underscores
318 $cleanFileName = preg_replace('/[' . self
::UNSAFE_FILENAME_CHARACTER_EXPRESSION
. '\\xC0-\\xFF]/', '_', trim($fileName));
320 // Strip trailing dots and return
321 $cleanFileName = rtrim($cleanFileName, '.');
322 if ($cleanFileName === '') {
323 throw new Exception\
InvalidFileNameException(
324 'File name ' . $fileName . ' is invalid.',
328 return $cleanFileName;
332 * Generic wrapper for extracting a list of items from a path.
334 * @param string $folderIdentifier
335 * @param int $start The position to start the listing; if not set, start from the beginning
336 * @param int $numberOfItems The number of items to list; if set to zero, all items are returned
337 * @param array $filterMethods The filter methods used to filter the directory items
338 * @param bool $includeFiles
339 * @param bool $includeDirs
340 * @param bool $recursive
341 * @param string $sort Property name used to sort the items.
342 * Among them may be: '' (empty, no sorting), name,
343 * fileext, size, tstamp and rw.
344 * If a driver does not support the given property, it
345 * should fall back to "name".
346 * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
348 * @throws \InvalidArgumentException
350 protected function getDirectoryItemList($folderIdentifier, $start = 0, $numberOfItems = 0, array $filterMethods, $includeFiles = TRUE, $includeDirs = TRUE, $recursive = FALSE, $sort = '', $sortRev = FALSE) {
351 $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
352 $realPath = $this->getAbsolutePath($folderIdentifier);
353 if (!is_dir($realPath)) {
354 throw new \
InvalidArgumentException(
355 'Cannot list items in directory ' . $folderIdentifier . ' - does not exist or is no directory',
364 $items = $this->retrieveFileAndFoldersInPath($realPath, $recursive, $includeFiles, $includeDirs, $sort, $sortRev);
365 $iterator = new \
ArrayIterator($items);
366 if ($iterator->count() === 0) {
369 $iterator->seek($start);
371 // $c is the counter for how many items we still have to fetch (-1 is unlimited)
372 $c = $numberOfItems > 0 ?
$numberOfItems : - 1;
374 while ($iterator->valid() && ($numberOfItems === 0 ||
$c > 0)) {
375 // $iteratorItem is the file or folder name
376 $iteratorItem = $iterator->current();
377 // go on to the next iterator item now as we might skip this one early
381 !$this->applyFilterMethodsToDirectoryItem(
383 $iteratorItem['name'],
384 $iteratorItem['identifier'],
385 $this->getParentFolderIdentifierOfIdentifier($iteratorItem['identifier'])
391 $items[$iteratorItem['identifier']] = $iteratorItem['identifier'];
392 // Decrement item counter to make sure we only return $numberOfItems
393 // we cannot do this earlier in the method (unlike moving the iterator forward) because we only add the
401 * Applies a set of filter methods to a file name to find out if it should be used or not. This is e.g. used by
402 * directory listings.
404 * @param array $filterMethods The filter methods to use
405 * @param string $itemName
406 * @param string $itemIdentifier
407 * @param string $parentIdentifier
408 * @throws \RuntimeException
411 protected function applyFilterMethodsToDirectoryItem(array $filterMethods, $itemName, $itemIdentifier, $parentIdentifier) {
412 foreach ($filterMethods as $filter) {
413 if (is_callable($filter)) {
414 $result = call_user_func($filter, $itemName, $itemIdentifier, $parentIdentifier, array(), $this);
415 // We have to use -1 as the „don't include“ return value, as call_user_func() will return FALSE
416 // If calling the method succeeded and thus we can't use that as a return value.
417 if ($result === -1) {
419 } elseif ($result === FALSE) {
420 throw new \
RuntimeException('Could not apply file/folder name filter ' . $filter[0] . '::' . $filter[1]);
428 * Returns a list of files inside the specified path
430 * @param string $folderIdentifier
432 * @param int $numberOfItems
433 * @param bool $recursive
434 * @param array $filenameFilterCallbacks The method callbacks to use for filtering the items
435 * @param string $sort Property name used to sort the items.
436 * Among them may be: '' (empty, no sorting), name,
437 * fileext, size, tstamp and rw.
438 * If a driver does not support the given property, it
439 * should fall back to "name".
440 * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
441 * @return array of FileIdentifiers
443 public function getFilesInFolder($folderIdentifier, $start = 0, $numberOfItems = 0, $recursive = FALSE, array $filenameFilterCallbacks = array(), $sort = '', $sortRev = FALSE) {
444 return $this->getDirectoryItemList($folderIdentifier, $start, $numberOfItems, $filenameFilterCallbacks, TRUE, FALSE, $recursive, $sort, $sortRev);
448 * Returns the number of files inside the specified path
450 * @param string $folderIdentifier
451 * @param bool $recursive
452 * @param array $filenameFilterCallbacks callbacks for filtering the items
453 * @return int Number of files in folder
455 public function countFilesInFolder($folderIdentifier, $recursive = FALSE, array $filenameFilterCallbacks = array()) {
456 return count($this->getFilesInFolder($folderIdentifier, 0, 0, $recursive, $filenameFilterCallbacks));
460 * Returns a list of folders inside the specified path
462 * @param string $folderIdentifier
464 * @param int $numberOfItems
465 * @param bool $recursive
466 * @param array $folderNameFilterCallbacks The method callbacks to use for filtering the items
467 * @param string $sort Property name used to sort the items.
468 * Among them may be: '' (empty, no sorting), name,
469 * fileext, size, tstamp and rw.
470 * If a driver does not support the given property, it
471 * should fall back to "name".
472 * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
473 * @return array of Folder Identifier
475 public function getFoldersInFolder($folderIdentifier, $start = 0, $numberOfItems = 0, $recursive = FALSE, array $folderNameFilterCallbacks = array(), $sort = '', $sortRev = FALSE) {
476 return $this->getDirectoryItemList($folderIdentifier, $start, $numberOfItems, $folderNameFilterCallbacks, FALSE, TRUE, $recursive, $sort, $sortRev);
480 * Returns the number of folders inside the specified path
482 * @param string $folderIdentifier
483 * @param bool $recursive
484 * @param array $folderNameFilterCallbacks callbacks for filtering the items
485 * @return integer Number of folders in folder
487 public function countFoldersInFolder($folderIdentifier, $recursive = FALSE, array $folderNameFilterCallbacks = array()) {
488 return count($this->getFoldersInFolder($folderIdentifier, 0, 0, $recursive, $folderNameFilterCallbacks));
492 * Returns a list with the names of all files and folders in a path, optionally recursive.
494 * @param string $path The absolute path
495 * @param bool $recursive If TRUE, recursively fetches files and folders
496 * @param bool $includeFiles
497 * @param bool $includeDirs
498 * @param string $sort Property name used to sort the items.
499 * Among them may be: '' (empty, no sorting), name,
500 * fileext, size, tstamp and rw.
501 * If a driver does not support the given property, it
502 * should fall back to "name".
503 * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
506 protected function retrieveFileAndFoldersInPath($path, $recursive = FALSE, $includeFiles = TRUE, $includeDirs = TRUE, $sort = '', $sortRev = FALSE) {
507 $pathLength = strlen($this->getAbsoluteBasePath());
508 $iteratorMode = \FilesystemIterator
::UNIX_PATHS | \FilesystemIterator
::SKIP_DOTS | \FilesystemIterator
::CURRENT_AS_FILEINFO | \FilesystemIterator
::FOLLOW_SYMLINKS
;
510 $iterator = new \
RecursiveIteratorIterator(
511 new \
RecursiveDirectoryIterator($path, $iteratorMode),
512 \RecursiveIteratorIterator
::SELF_FIRST
515 $iterator = new \
RecursiveDirectoryIterator($path, $iteratorMode);
518 $directoryEntries = array();
519 while ($iterator->valid()) {
520 /** @var $entry \SplFileInfo */
521 $entry = $iterator->current();
522 // skip non-files/non-folders, and empty entries
523 if ((!$entry->isFile() && !$entry->isDir()) ||
$entry->getFilename() == '' ||
524 ($entry->isFile() && !$includeFiles) ||
($entry->isDir() && !$includeDirs)) {
528 $entryIdentifier = '/' . substr($entry->getPathname(), $pathLength);
529 $entryName = PathUtility
::basename($entryIdentifier);
530 if ($entry->isDir()) {
531 $entryIdentifier .= '/';
534 'identifier' => $entryIdentifier,
535 'name' => $entryName,
536 'type' => $entry->isDir() ?
'dir' : 'file'
538 $directoryEntries[$entryIdentifier] = $entryArray;
541 return $this->sortDirectoryEntries($directoryEntries, $sort, $sortRev);
545 * Sort the directory entries by a certain key
547 * @param array $directoryEntries Array of directory entry arrays from
548 * retrieveFileAndFoldersInPath()
549 * @param string $sort Property name used to sort the items.
550 * Among them may be: '' (empty, no sorting), name,
551 * fileext, size, tstamp and rw.
552 * If a driver does not support the given property, it
553 * should fall back to "name".
554 * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
555 * @return array Sorted entries. Content of the keys is undefined.
557 protected function sortDirectoryEntries($directoryEntries, $sort = '', $sortRev = FALSE) {
558 $entriesToSort = array();
559 foreach ($directoryEntries as $entryArray) {
560 $dir = pathinfo($entryArray['name'], PATHINFO_DIRNAME
) . '/';
561 $fullPath = $this->getAbsoluteBasePath() . $entryArray['identifier'];
564 if ($entryArray['type'] === 'file') {
565 $sortingKey = $this->getSpecificFileInformation($fullPath, $dir, 'size');
571 $perms = $this->getPermissions($entryArray['identifier']);
572 $sortingKey = ($perms['r'] ?
'R' : '')
573 . ($perms['w'] ?
'W' : '');
576 $sortingKey = pathinfo($entryArray['name'], PATHINFO_EXTENSION
);
579 if ($entryArray['type'] === 'file') {
580 $sortingKey = $this->getSpecificFileInformation($fullPath, $dir, 'mtime');
588 $sortingKey = $entryArray['name'];
591 while (isset($entriesToSort[$sortingKey . $i])) {
594 $entriesToSort[$sortingKey . $i] = $entryArray;
596 uksort($entriesToSort, 'strnatcasecmp');
599 $entriesToSort = array_reverse($entriesToSort);
602 return $entriesToSort;
606 * Extracts information about a file from the filesystem.
608 * @param string $filePath The absolute path to the file
609 * @param string $containerPath The relative path to the file's container
610 * @param array $propertiesToExtract array of properties which should be returned, if empty all will be extracted
613 protected function extractFileInformation($filePath, $containerPath, array $propertiesToExtract = array()) {
614 if (count($propertiesToExtract) === 0) {
615 $propertiesToExtract = array(
616 'size', 'atime', 'atime', 'mtime', 'ctime', 'mimetype', 'name',
617 'identifier', 'identifier_hash', 'storage', 'folder_hash'
620 $fileInformation = array();
621 foreach ($propertiesToExtract as $property) {
622 $fileInformation[$property] = $this->getSpecificFileInformation($filePath, $containerPath, $property);
624 return $fileInformation;
629 * Extracts a specific FileInformation from the FileSystems.
631 * @param string $fileIdentifier
632 * @param string $containerPath
633 * @param string $property
635 * @return bool|int|string
636 * @throws \InvalidArgumentException
638 public function getSpecificFileInformation($fileIdentifier, $containerPath, $property) {
639 $identifier = $this->canonicalizeAndCheckFileIdentifier($containerPath . PathUtility
::basename($fileIdentifier));
641 $fileInfo = GeneralUtility
::makeInstance(FileInfo
::class, $fileIdentifier);
644 return $fileInfo->getSize();
646 return $fileInfo->getATime();
648 return $fileInfo->getMTime();
650 return $fileInfo->getCTime();
652 return PathUtility
::basename($fileIdentifier);
654 return $fileInfo->getMimeType();
658 return $this->storageUid
;
659 case 'identifier_hash':
660 return $this->hashIdentifier($identifier);
662 return $this->hashIdentifier($this->getParentFolderIdentifierOfIdentifier($identifier));
664 throw new \
InvalidArgumentException(sprintf('The information "%s" is not available.', $property));
669 * Returns the absolute path of the folder this driver operates on.
673 protected function getAbsoluteBasePath() {
674 return $this->absoluteBasePath
;
678 * Returns the absolute path of a file or folder.
680 * @param string $fileIdentifier
682 * @throws Exception\InvalidPathException
684 protected function getAbsolutePath($fileIdentifier) {
685 $relativeFilePath = ltrim($this->canonicalizeAndCheckFileIdentifier($fileIdentifier), '/');
686 $path = $this->absoluteBasePath
. $relativeFilePath;
691 * Creates a (cryptographic) hash for a file.
693 * @param string $fileIdentifier
694 * @param string $hashAlgorithm The hash algorithm to use
696 * @throws \RuntimeException
697 * @throws \InvalidArgumentException
699 public function hash($fileIdentifier, $hashAlgorithm) {
700 if (!in_array($hashAlgorithm, $this->supportedHashAlgorithms
)) {
701 throw new \
InvalidArgumentException('Hash algorithm "' . $hashAlgorithm . '" is not supported.', 1304964032);
703 switch ($hashAlgorithm) {
705 $hash = sha1_file($this->getAbsolutePath($fileIdentifier));
708 $hash = md5_file($this->getAbsolutePath($fileIdentifier));
711 throw new \
RuntimeException('Hash algorithm ' . $hashAlgorithm . ' is not implemented.', 1329644451);
717 * Adds a file from the local server hard disk to a given path in TYPO3s virtual file system.
718 * This assumes that the local file exists, so no further check is done here!
719 * After a successful the original file must not exist anymore.
721 * @param string $localFilePath (within PATH_site)
722 * @param string $targetFolderIdentifier
723 * @param string $newFileName optional, if not given original name is used
724 * @param bool $removeOriginal if set the original file will be removed after successful operation
725 * @return string the identifier of the new file
726 * @throws \RuntimeException
727 * @throws \InvalidArgumentException
729 public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = '', $removeOriginal = TRUE) {
730 $localFilePath = $this->canonicalizeAndCheckFilePath($localFilePath);
731 // as for the "virtual storage" for backwards-compatibility, this check always fails, as the file probably lies under PATH_site
732 // thus, it is not checked here
733 // @todo is check in storage
734 if (GeneralUtility
::isFirstPartOfStr($localFilePath, $this->absoluteBasePath
) && $this->storageUid
> 0) {
735 throw new \
InvalidArgumentException('Cannot add a file that is already part of this storage.', 1314778269);
737 $newFileName = $this->sanitizeFileName($newFileName !== '' ?
$newFileName : PathUtility
::basename($localFilePath));
738 $newFileIdentifier = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier) . $newFileName;
739 $targetPath = $this->getAbsolutePath($newFileIdentifier);
741 if ($removeOriginal) {
742 if (is_uploaded_file($localFilePath)) {
743 $result = move_uploaded_file($localFilePath, $targetPath);
745 $result = rename($localFilePath, $targetPath);
748 $result = copy($localFilePath, $targetPath);
750 if ($result === FALSE ||
!file_exists($targetPath)) {
751 throw new \
RuntimeException('Adding file ' . $localFilePath . ' at ' . $newFileIdentifier . ' failed.');
754 // Change the permissions of the file
755 GeneralUtility
::fixPermissions($targetPath);
756 return $newFileIdentifier;
760 * Checks if a file exists.
762 * @param string $fileIdentifier
766 public function fileExists($fileIdentifier) {
767 $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
768 return is_file($absoluteFilePath);
772 * Checks if a file inside a folder exists
774 * @param string $fileName
775 * @param string $folderIdentifier
778 public function fileExistsInFolder($fileName, $folderIdentifier) {
779 $identifier = $folderIdentifier . '/' . $fileName;
780 $identifier = $this->canonicalizeAndCheckFileIdentifier($identifier);
781 return $this->fileExists($identifier);
785 * Checks if a folder exists.
787 * @param string $folderIdentifier
791 public function folderExists($folderIdentifier) {
792 $absoluteFilePath = $this->getAbsolutePath($folderIdentifier);
793 return is_dir($absoluteFilePath);
797 * Checks if a folder inside a folder exists.
799 * @param string $folderName
800 * @param string $folderIdentifier
803 public function folderExistsInFolder($folderName, $folderIdentifier) {
804 $identifier = $folderIdentifier . '/' . $folderName;
805 $identifier = $this->canonicalizeAndCheckFolderIdentifier($identifier);
806 return $this->folderExists($identifier);
810 * Returns the Identifier for a folder within a given folder.
812 * @param string $folderName The name of the target folder
813 * @param string $folderIdentifier
816 public function getFolderInFolder($folderName, $folderIdentifier) {
817 $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier . '/' . $folderName);
818 return $folderIdentifier;
822 * Replaces the contents (and file-specific metadata) of a file object with a local file.
824 * @param string $fileIdentifier
825 * @param string $localFilePath
826 * @return bool TRUE if the operation succeeded
827 * @throws \RuntimeException
829 public function replaceFile($fileIdentifier, $localFilePath) {
830 $filePath = $this->getAbsolutePath($fileIdentifier);
831 $result = rename($localFilePath, $filePath);
832 if ($result === FALSE) {
833 throw new \
RuntimeException('Replacing file ' . $fileIdentifier . ' with ' . $localFilePath . ' failed.', 1315314711);
839 * Copies a file *within* the current storage.
840 * Note that this is only about an intra-storage copy action, where a file is just
841 * copied to another folder in the same storage.
843 * @param string $fileIdentifier
844 * @param string $targetFolderIdentifier
845 * @param string $fileName
846 * @return string the Identifier of the new file
848 public function copyFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $fileName) {
849 $sourcePath = $this->getAbsolutePath($fileIdentifier);
850 $newIdentifier = $targetFolderIdentifier . '/' . $fileName;
851 $newIdentifier = $this->canonicalizeAndCheckFileIdentifier($newIdentifier);
853 $absoluteFilePath = $this->getAbsolutePath($newIdentifier);
854 copy($sourcePath, $absoluteFilePath);
855 GeneralUtility
::fixPermissions($absoluteFilePath);
856 return $newIdentifier;
860 * Moves a file *within* the current storage.
861 * Note that this is only about an inner-storage move action, where a file is just
862 * moved to another folder in the same storage.
864 * @param string $fileIdentifier
865 * @param string $targetFolderIdentifier
866 * @param string $newFileName
868 * @throws \RuntimeException
870 public function moveFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $newFileName) {
871 $sourcePath = $this->getAbsolutePath($fileIdentifier);
872 $targetIdentifier = $targetFolderIdentifier . '/' . $newFileName;
873 $targetIdentifier = $this->canonicalizeAndCheckFileIdentifier($targetIdentifier);
874 $result = rename($sourcePath, $this->getAbsolutePath($targetIdentifier));
875 if ($result === FALSE) {
876 throw new \
RuntimeException('Moving file ' . $sourcePath . ' to ' . $targetIdentifier . ' failed.', 1315314712);
878 return $targetIdentifier;
882 * Copies a file to a temporary path and returns that path.
884 * @param string $fileIdentifier
885 * @return string The temporary path
886 * @throws \RuntimeException
888 protected function copyFileToTemporaryPath($fileIdentifier) {
889 $sourcePath = $this->getAbsolutePath($fileIdentifier);
890 $temporaryPath = $this->getTemporaryPathForFile($fileIdentifier);
891 $result = copy($sourcePath, $temporaryPath);
892 touch($temporaryPath, filemtime($sourcePath));
893 if ($result === FALSE) {
894 throw new \
RuntimeException(
895 'Copying file "' . $fileIdentifier . '" to temporary path "' . $temporaryPath . '" failed.',
899 return $temporaryPath;
903 * Creates a map of old and new file/folder identifiers after renaming or
904 * moving a folder. The old identifier is used as the key, the new one as the value.
906 * @param array $filesAndFolders
907 * @param string $sourceFolderIdentifier
908 * @param string $targetFolderIdentifier
911 * @throws Exception\FileOperationErrorException
913 protected function createIdentifierMap(array $filesAndFolders, $sourceFolderIdentifier, $targetFolderIdentifier) {
914 $identifierMap = array();
915 $identifierMap[$sourceFolderIdentifier] = $targetFolderIdentifier;
916 foreach ($filesAndFolders as $oldItem) {
917 if ($oldItem['type'] == 'dir') {
918 $oldIdentifier = $oldItem['identifier'];
919 $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier(
920 str_replace($sourceFolderIdentifier, $targetFolderIdentifier, $oldItem['identifier'])
923 $oldIdentifier = $oldItem['identifier'];
924 $newIdentifier = $this->canonicalizeAndCheckFileIdentifier(
925 str_replace($sourceFolderIdentifier, $targetFolderIdentifier, $oldItem['identifier'])
928 if (!file_exists($this->getAbsolutePath($newIdentifier))) {
929 throw new Exception\
FileOperationErrorException(
930 sprintf('File "%1$s" was not found (should have been copied/moved from "%2$s").', $newIdentifier, $oldIdentifier),
934 $identifierMap[$oldIdentifier] = $newIdentifier;
936 return $identifierMap;
940 * Folder equivalent to moveFileWithinStorage().
942 * @param string $sourceFolderIdentifier
943 * @param string $targetFolderIdentifier
944 * @param string $newFolderName
946 * @return array A map of old to new file identifiers
947 * @throws \RuntimeException
949 public function moveFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName) {
950 $sourcePath = $this->getAbsolutePath($sourceFolderIdentifier);
951 $relativeTargetPath = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier . '/' . $newFolderName);
952 $targetPath = $this->getAbsolutePath($relativeTargetPath);
953 // get all files and folders we are going to move, to have a map for updating later.
954 $filesAndFolders = $this->retrieveFileAndFoldersInPath($sourcePath, TRUE);
955 $result = rename($sourcePath, $targetPath);
956 if ($result === FALSE) {
957 throw new \
RuntimeException('Moving folder ' . $sourcePath . ' to ' . $targetPath . ' failed.', 1320711817);
959 // Create a mapping from old to new identifiers
960 $identifierMap = $this->createIdentifierMap($filesAndFolders, $sourceFolderIdentifier, $relativeTargetPath);
961 return $identifierMap;
965 * Folder equivalent to copyFileWithinStorage().
967 * @param string $sourceFolderIdentifier
968 * @param string $targetFolderIdentifier
969 * @param string $newFolderName
972 * @throws Exception\FileOperationErrorException
974 public function copyFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName) {
975 // This target folder path already includes the topmost level, i.e. the folder this method knows as $folderToCopy.
976 // We can thus rely on this folder being present and just create the subfolder we want to copy to.
977 $newFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier . '/' . $newFolderName);
978 $sourceFolderPath = $this->getAbsolutePath($sourceFolderIdentifier);
979 $targetFolderPath = $this->getAbsolutePath($newFolderIdentifier);
981 mkdir($targetFolderPath);
982 /** @var $iterator \RecursiveDirectoryIterator */
983 $iterator = new \
RecursiveIteratorIterator(
984 new \
RecursiveDirectoryIterator($sourceFolderPath),
985 \RecursiveIteratorIterator
::SELF_FIRST
987 // Rewind the iterator as this is important for some systems e.g. Windows
989 while ($iterator->valid()) {
990 /** @var $current \RecursiveDirectoryIterator */
991 $current = $iterator->current();
992 $fileName = $current->getFilename();
993 $itemSubPath = GeneralUtility
::fixWindowsFilePath($iterator->getSubPathname());
994 if ($current->isDir() && !($fileName === '..' ||
$fileName === '.')) {
995 GeneralUtility
::mkdir($targetFolderPath . '/' . $itemSubPath);
996 } elseif ($current->isFile()) {
997 $result = copy($sourceFolderPath . '/' . $itemSubPath, $targetFolderPath . '/' . $itemSubPath);
998 if ($result === FALSE) {
1000 GeneralUtility
::rmdir($targetFolderIdentifier, TRUE);
1001 throw new Exception\
FileOperationErrorException(
1002 'Copying file "' . $sourceFolderPath . $itemSubPath . '" to "' . $targetFolderPath . $itemSubPath . '" failed.',
1010 GeneralUtility
::fixPermissions($targetFolderPath, TRUE);
1015 * Renames a file in this storage.
1017 * @param string $fileIdentifier
1018 * @param string $newName The target path (including the file name!)
1019 * @return string The identifier of the file after renaming
1020 * @throws Exception\ExistingTargetFileNameException
1021 * @throws \RuntimeException
1023 public function renameFile($fileIdentifier, $newName) {
1024 // Makes sure the Path given as parameter is valid
1025 $newName = $this->sanitizeFileName($newName);
1026 $newIdentifier = rtrim(GeneralUtility
::fixWindowsFilePath(PathUtility
::dirname($fileIdentifier)), '/') . '/' . $newName;
1027 $newIdentifier = $this->canonicalizeAndCheckFileIdentifier($newIdentifier);
1028 // The target should not exist already
1029 if ($this->fileExists($newIdentifier)) {
1030 throw new Exception\
ExistingTargetFileNameException(
1031 'The target file "' . $newIdentifier . '" already exists.',
1035 $sourcePath = $this->getAbsolutePath($fileIdentifier);
1036 $targetPath = $this->getAbsolutePath($newIdentifier);
1037 $result = rename($sourcePath, $targetPath);
1038 if ($result === FALSE) {
1039 throw new \
RuntimeException('Renaming file ' . $sourcePath . ' to ' . $targetPath . ' failed.', 1320375115);
1041 return $newIdentifier;
1046 * Renames a folder in this storage.
1048 * @param string $folderIdentifier
1049 * @param string $newName
1050 * @return array A map of old to new file identifiers of all affected files and folders
1051 * @throws \RuntimeException if renaming the folder failed
1053 public function renameFolder($folderIdentifier, $newName) {
1054 $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
1055 $newName = $this->sanitizeFileName($newName);
1057 $newIdentifier = PathUtility
::dirname($folderIdentifier) . '/' . $newName;
1058 $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier($newIdentifier);
1060 $sourcePath = $this->getAbsolutePath($folderIdentifier);
1061 $targetPath = $this->getAbsolutePath($newIdentifier);
1062 // get all files and folders we are going to move, to have a map for updating later.
1063 $filesAndFolders = $this->retrieveFileAndFoldersInPath($sourcePath, TRUE);
1064 $result = rename($sourcePath, $targetPath);
1065 if ($result === FALSE) {
1066 throw new \
RuntimeException(sprintf('Renaming folder "%1$s" to "%2$s" failed."', $sourcePath, $targetPath), 1320375116);
1069 // Create a mapping from old to new identifiers
1070 $identifierMap = $this->createIdentifierMap($filesAndFolders, $folderIdentifier, $newIdentifier);
1071 } catch (\Exception
$e) {
1072 rename($targetPath, $sourcePath);
1073 throw new \
RuntimeException(
1075 'Creating filename mapping after renaming "%1$s" to "%2$s" failed. Reverted rename operation.\\n\\nOriginal error: %3$s"',
1076 $sourcePath, $targetPath, $e->getMessage()
1081 return $identifierMap;
1085 * Removes a file from the filesystem. This does not check if the file is
1086 * still used or if it is a bad idea to delete it for some other reason
1087 * this has to be taken care of in the upper layers (e.g. the Storage)!
1089 * @param string $fileIdentifier
1090 * @return bool TRUE if deleting the file succeeded
1091 * @throws \RuntimeException
1093 public function deleteFile($fileIdentifier) {
1094 $filePath = $this->getAbsolutePath($fileIdentifier);
1095 $result = unlink($filePath);
1096 if ($result === FALSE) {
1097 throw new \
RuntimeException('Deletion of file ' . $fileIdentifier . ' failed.', 1320855304);
1103 * Removes a folder from this storage.
1105 * @param string $folderIdentifier
1106 * @param bool $deleteRecursively
1108 * @throws Exception\FileOperationErrorException
1109 * @throws Exception\InvalidPathException
1111 public function deleteFolder($folderIdentifier, $deleteRecursively = FALSE) {
1112 $folderPath = $this->getAbsolutePath($folderIdentifier);
1113 $result = GeneralUtility
::rmdir($folderPath, $deleteRecursively);
1114 if ($result === FALSE) {
1115 throw new Exception\
FileOperationErrorException(
1116 'Deleting folder "' . $folderIdentifier . '" failed.',
1124 * Checks if a folder contains files and (if supported) other folders.
1126 * @param string $folderIdentifier
1127 * @return bool TRUE if there are no files and folders within $folder
1129 public function isFolderEmpty($folderIdentifier) {
1130 $path = $this->getAbsolutePath($folderIdentifier);
1131 $dirHandle = opendir($path);
1132 while ($entry = readdir($dirHandle)) {
1133 if ($entry !== '.' && $entry !== '..') {
1134 closedir($dirHandle);
1138 closedir($dirHandle);
1143 * Returns (a local copy of) a file for processing it. This makes a copy
1144 * first when in writable mode, so if you change the file, you have to update it yourself afterwards.
1146 * @param string $fileIdentifier
1147 * @param bool $writable Set this to FALSE if you only need the file for read operations.
1148 * This might speed up things, e.g. by using a cached local version.
1149 * Never modify the file if you have set this flag!
1150 * @return string The path to the file on the local disk
1152 public function getFileForLocalProcessing($fileIdentifier, $writable = TRUE) {
1153 if ($writable === FALSE) {
1154 return $this->getAbsolutePath($fileIdentifier);
1156 return $this->copyFileToTemporaryPath($fileIdentifier);
1162 * Returns the permissions of a file/folder as an array (keys r, w) of boolean flags
1164 * @param string $identifier
1166 * @throws \RuntimeException
1168 public function getPermissions($identifier) {
1169 $path = $this->getAbsolutePath($identifier);
1170 $permissionBits = fileperms($path);
1171 if ($permissionBits === FALSE) {
1172 throw new \
RuntimeException('Error while fetching permissions for ' . $path, 1319455097);
1175 'r' => (bool)is_readable($path),
1176 'w' => (bool)is_writable($path)
1181 * Checks if a given identifier is within a container, e.g. if
1182 * a file or folder is within another folder. It will also return
1183 * TRUE if both canonicalized identifiers are equal.
1185 * @param string $folderIdentifier
1186 * @param string $identifier identifier to be checked against $folderIdentifier
1187 * @return bool TRUE if $content is within or matches $folderIdentifier
1189 public function isWithin($folderIdentifier, $identifier) {
1190 $folderIdentifier = $this->canonicalizeAndCheckFileIdentifier($folderIdentifier);
1191 $entryIdentifier = $this->canonicalizeAndCheckFileIdentifier($identifier);
1192 if ($folderIdentifier === $entryIdentifier) {
1195 // File identifier canonicalization will not modify a single slash so
1196 // we must not append another slash in that case.
1197 if ($folderIdentifier !== '/') {
1198 $folderIdentifier .= '/';
1200 return GeneralUtility
::isFirstPartOfStr($entryIdentifier, $folderIdentifier);
1204 * Creates a new (empty) file and returns the identifier.
1206 * @param string $fileName
1207 * @param string $parentFolderIdentifier
1209 * @throws Exception\InvalidFileNameException
1210 * @throws \RuntimeException
1212 public function createFile($fileName, $parentFolderIdentifier) {
1213 if (!$this->isValidFilename($fileName)) {
1214 throw new Exception\
InvalidFileNameException(
1215 'Invalid characters in fileName "' . $fileName . '"',
1219 $parentFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier);
1220 $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier(
1221 $parentFolderIdentifier . $this->sanitizeFileName(ltrim($fileName, '/'))
1223 $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
1224 $result = touch($absoluteFilePath);
1225 GeneralUtility
::fixPermissions($absoluteFilePath);
1227 if ($result !== TRUE) {
1228 throw new \
RuntimeException('Creating file ' . $fileIdentifier . ' failed.', 1320569854);
1230 return $fileIdentifier;
1234 * Returns the contents of a file. Beware that this requires to load the
1235 * complete file into memory and also may require fetching the file from an
1236 * external location. So this might be an expensive operation (both in terms of
1237 * processing resources and money) for large files.
1239 * @param string $fileIdentifier
1240 * @return string The file contents
1242 public function getFileContents($fileIdentifier) {
1243 $filePath = $this->getAbsolutePath($fileIdentifier);
1244 return file_get_contents($filePath);
1248 * Sets the contents of a file to the specified value.
1250 * @param string $fileIdentifier
1251 * @param string $contents
1252 * @return int The number of bytes written to the file
1253 * @throws \RuntimeException if the operation failed
1255 public function setFileContents($fileIdentifier, $contents) {
1256 $filePath = $this->getAbsolutePath($fileIdentifier);
1257 $result = file_put_contents($filePath, $contents);
1259 // Make sure later calls to filesize() etc. return correct values.
1260 clearstatcache(TRUE, $filePath);
1262 if ($result === FALSE) {
1263 throw new \
RuntimeException('Setting contents of file "' . $fileIdentifier . '" failed.', 1325419305);
1269 * Gets the charset conversion object.
1271 * @return \TYPO3\CMS\Core\Charset\CharsetConverter
1273 protected function getCharsetConversion() {
1274 if (!isset($this->charsetConversion
)) {
1275 if (TYPO3_MODE
=== 'FE') {
1276 $this->charsetConversion
= $GLOBALS['TSFE']->csConvObj
;
1277 } elseif (is_object($GLOBALS['LANG'])) {
1279 $this->charsetConversion
= $GLOBALS['LANG']->csConvObj
;
1281 // The object may not exist yet, so we need to create it now. Happens in the Install Tool for example.
1282 $this->charsetConversion
= GeneralUtility
::makeInstance(\TYPO3\CMS\Core\Charset\CharsetConverter
::class);
1285 return $this->charsetConversion
;
1289 * Returns the role of an item (currently only folders; can later be extended for files as well)
1291 * @param string $folderIdentifier
1294 public function getRole($folderIdentifier) {
1295 $name = PathUtility
::basename($folderIdentifier);
1296 $role = $this->mappingFolderNameToRole
[$name];
1298 $role = FolderInterface
::ROLE_DEFAULT
;
1304 * Directly output the contents of the file to the output
1305 * buffer. Should not take care of header files or flushing
1306 * buffer before. Will be taken care of by the Storage.
1308 * @param string $identifier
1311 public function dumpFileContents($identifier) {
1312 readfile($this->getAbsolutePath($this->canonicalizeAndCheckFileIdentifier($identifier)), 0);