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 Psr\Http\Message\ResponseInterface
;
18 use TYPO3\CMS\Core\Charset\CharsetConverter
;
19 use TYPO3\CMS\Core\Core\Environment
;
20 use TYPO3\CMS\Core\Http\Response
;
21 use TYPO3\CMS\Core\Http\SelfEmittableLazyOpenStream
;
22 use TYPO3\CMS\Core\
Resource\Exception
;
23 use TYPO3\CMS\Core\
Resource\FolderInterface
;
24 use TYPO3\CMS\Core\
Resource\ResourceStorage
;
25 use TYPO3\CMS\Core\Type\File\FileInfo
;
26 use TYPO3\CMS\Core\Utility\GeneralUtility
;
27 use TYPO3\CMS\Core\Utility\PathUtility
;
30 * Driver for the local file system
32 class LocalDriver
extends AbstractHierarchicalFilesystemDriver
implements StreamableDriverInterface
37 const UNSAFE_FILENAME_CHARACTER_EXPRESSION
= '\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF';
40 * The absolute base path. It always contains a trailing slash.
44 protected $absoluteBasePath;
47 * A list of all supported hash algorithms, written all lower case.
51 protected $supportedHashAlgorithms = ['sha1', 'md5'];
54 * The base URL that points to this driver's storage. As long is this
55 * is not set, it is assumed that this folder is not publicly available
62 protected $mappingFolderNameToRole = [
63 '_recycler_' => FolderInterface
::ROLE_RECYCLER
,
64 '_temp_' => FolderInterface
::ROLE_TEMPORARY
,
65 'user_upload' => FolderInterface
::ROLE_USERUPLOAD
,
69 * @param array $configuration
71 public function __construct(array $configuration = [])
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)
91 $this->capabilities
&= $capabilities;
92 return $this->capabilities
;
96 * Processes the configuration for this driver.
98 public function processConfiguration()
100 $this->absoluteBasePath
= $this->calculateBasePath($this->configuration
);
101 $this->determineBaseUrl();
102 if ($this->baseUri
=== null) {
103 // remove public flag
104 $this->capabilities
&= ~ResourceStorage
::CAPABILITY_PUBLIC
;
109 * Initializes this object. This is called by the storage after the driver
112 public function initialize()
117 * Determines the base URL for this driver, from the configuration or
118 * the TypoScript frontend object
120 protected function determineBaseUrl()
122 // only calculate baseURI if the storage does not enforce jumpUrl Script
123 if ($this->hasCapability(ResourceStorage
::CAPABILITY_PUBLIC
)) {
124 if (GeneralUtility
::isFirstPartOfStr($this->absoluteBasePath
, Environment
::getPublicPath())) {
125 // use site-relative URLs
126 $temporaryBaseUri = rtrim(PathUtility
::stripPathSitePrefix($this->absoluteBasePath
), '/');
127 if ($temporaryBaseUri !== '') {
128 $uriParts = explode('/', $temporaryBaseUri);
129 $uriParts = array_map('rawurlencode', $uriParts);
130 $temporaryBaseUri = implode('/', $uriParts) . '/';
132 $this->baseUri
= $temporaryBaseUri;
133 } elseif (isset($this->configuration
['baseUri']) && GeneralUtility
::isValidUrl($this->configuration
['baseUri'])) {
134 $this->baseUri
= rtrim($this->configuration
['baseUri'], '/') . '/';
140 * Calculates the absolute path to this drivers storage location.
142 * @throws Exception\InvalidConfigurationException
143 * @param array $configuration
145 * @throws Exception\InvalidPathException
147 protected function calculateBasePath(array $configuration)
149 if (!array_key_exists('basePath', $configuration) ||
empty($configuration['basePath'])) {
150 throw new Exception\
InvalidConfigurationException(
151 'Configuration must contain base path.',
156 if (!empty($configuration['pathType']) && $configuration['pathType'] === 'relative') {
157 $relativeBasePath = $configuration['basePath'];
158 $absoluteBasePath = Environment
::getPublicPath() . '/' . $relativeBasePath;
160 $absoluteBasePath = $configuration['basePath'];
162 $absoluteBasePath = $this->canonicalizeAndCheckFilePath($absoluteBasePath);
163 $absoluteBasePath = rtrim($absoluteBasePath, '/') . '/';
164 if (!is_dir($absoluteBasePath)) {
165 throw new Exception\
InvalidConfigurationException(
166 'Base path "' . $absoluteBasePath . '" does not exist or is no directory.',
170 return $absoluteBasePath;
174 * Returns the public URL to a file.
175 * For the local driver, this will always return a path relative to public web path.
177 * @param string $identifier
178 * @return string|null NULL if file is missing or deleted, the generated url otherwise
179 * @throws \TYPO3\CMS\Core\Resource\Exception
181 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()
204 * Returns identifier of the default folder new files should be put into.
208 public function getDefaultFolder()
210 $identifier = '/user_upload/';
211 $createFolder = !$this->folderExists($identifier);
212 if ($createFolder === true) {
213 $identifier = $this->createFolder('user_upload');
219 * Creates a folder, within a parent folder.
220 * If no parent folder is given, a rootlevel folder will be created
222 * @param string $newFolderName
223 * @param string $parentFolderIdentifier
224 * @param bool $recursive
225 * @return string the Identifier of the new folder
227 public function createFolder($newFolderName, $parentFolderIdentifier = '', $recursive = false)
229 $parentFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier);
230 $newFolderName = trim($newFolderName, '/');
231 if ($recursive == false) {
232 $newFolderName = $this->sanitizeFileName($newFolderName);
233 $newIdentifier = $parentFolderIdentifier . $newFolderName . '/';
234 GeneralUtility
::mkdir($this->getAbsolutePath($newIdentifier));
236 $parts = GeneralUtility
::trimExplode('/', $newFolderName);
237 $parts = array_map([$this, 'sanitizeFileName'], $parts);
238 $newFolderName = implode('/', $parts);
239 $newIdentifier = $parentFolderIdentifier . $newFolderName . '/';
240 GeneralUtility
::mkdir_deep($this->getAbsolutePath($parentFolderIdentifier) . '/' . $newFolderName);
242 return $newIdentifier;
246 * Returns information about a file.
248 * @param string $fileIdentifier In the case of the LocalDriver, this is the (relative) path to the file.
249 * @param array $propertiesToExtract Array of properties which should be extracted, if empty all will be extracted
251 * @throws \InvalidArgumentException
253 public function getFileInfoByIdentifier($fileIdentifier, array $propertiesToExtract = [])
255 $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
256 // don't use $this->fileExists() because we need the absolute path to the file anyways, so we can directly
257 // use PHP's filesystem method.
258 if (!file_exists($absoluteFilePath) ||
!is_file($absoluteFilePath)) {
259 throw new \
InvalidArgumentException('File ' . $fileIdentifier . ' does not exist.', 1314516809);
262 $dirPath = PathUtility
::dirname($fileIdentifier);
263 $dirPath = $this->canonicalizeAndCheckFolderIdentifier($dirPath);
264 return $this->extractFileInformation($absoluteFilePath, $dirPath, $propertiesToExtract);
268 * Returns information about a folder.
270 * @param string $folderIdentifier In the case of the LocalDriver, this is the (relative) path to the file.
272 * @throws Exception\FolderDoesNotExistException
274 public function getFolderInfoByIdentifier($folderIdentifier)
276 $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
278 if (!$this->folderExists($folderIdentifier)) {
279 throw new Exception\
FolderDoesNotExistException(
280 'Folder "' . $folderIdentifier . '" does not exist.',
284 $absolutePath = $this->getAbsolutePath($folderIdentifier);
286 'identifier' => $folderIdentifier,
287 'name' => PathUtility
::basename($folderIdentifier),
288 'mtime' => filemtime($absolutePath),
289 'ctime' => filectime($absolutePath),
290 'storage' => $this->storageUid
295 * Returns a string where any character not matching [.a-zA-Z0-9_-] is
297 * Trailing dots are removed
299 * Previously in \TYPO3\CMS\Core\Utility\File\BasicFileUtility::cleanFileName()
301 * @param string $fileName Input string, typically the body of a fileName
302 * @param string $charset Charset of the a fileName (defaults to utf-8)
303 * @return string Output string with any characters not matching [.a-zA-Z0-9_-] is substituted by '_' and trailing dots removed
304 * @throws Exception\InvalidFileNameException
306 public function sanitizeFileName($fileName, $charset = 'utf-8')
308 // Handle UTF-8 characters
309 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']) {
310 // Allow ".", "-", 0-9, a-z, A-Z and everything beyond U+C0 (latin capital letter a with grave)
311 $cleanFileName = preg_replace('/[' . self
::UNSAFE_FILENAME_CHARACTER_EXPRESSION
. ']/u', '_', trim($fileName));
313 $fileName = GeneralUtility
::makeInstance(CharsetConverter
::class)->specCharsToASCII($charset, $fileName);
314 // Replace unwanted characters with underscores
315 $cleanFileName = preg_replace('/[' . self
::UNSAFE_FILENAME_CHARACTER_EXPRESSION
. '\\xC0-\\xFF]/', '_', trim($fileName));
317 // Strip trailing dots and return
318 $cleanFileName = rtrim($cleanFileName, '.');
319 if ($cleanFileName === '') {
320 throw new Exception\
InvalidFileNameException(
321 'File name ' . $fileName . ' is invalid.',
325 return $cleanFileName;
329 * Generic wrapper for extracting a list of items from a path.
331 * @param string $folderIdentifier
332 * @param int $start The position to start the listing; if not set, start from the beginning
333 * @param int $numberOfItems The number of items to list; if set to zero, all items are returned
334 * @param array $filterMethods The filter methods used to filter the directory items
335 * @param bool $includeFiles
336 * @param bool $includeDirs
337 * @param bool $recursive
338 * @param string $sort Property name used to sort the items.
339 * Among them may be: '' (empty, no sorting), name,
340 * fileext, size, tstamp and rw.
341 * If a driver does not support the given property, it
342 * should fall back to "name".
343 * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
345 * @throws \InvalidArgumentException
347 protected function getDirectoryItemList($folderIdentifier, $start = 0, $numberOfItems = 0, array $filterMethods, $includeFiles = true, $includeDirs = true, $recursive = false, $sort = '', $sortRev = false)
349 $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
350 $realPath = $this->getAbsolutePath($folderIdentifier);
351 if (!is_dir($realPath)) {
352 throw new \
InvalidArgumentException(
353 'Cannot list items in directory ' . $folderIdentifier . ' - does not exist or is no directory',
358 $items = $this->retrieveFileAndFoldersInPath($realPath, $recursive, $includeFiles, $includeDirs, $sort, $sortRev);
359 $iterator = new \
ArrayIterator($items);
360 if ($iterator->count() === 0) {
364 // $c is the counter for how many items we still have to fetch (-1 is unlimited)
365 $c = $numberOfItems > 0 ?
$numberOfItems : - 1;
367 while ($iterator->valid() && ($numberOfItems === 0 ||
$c > 0)) {
368 // $iteratorItem is the file or folder name
369 $iteratorItem = $iterator->current();
370 // go on to the next iterator item now as we might skip this one early
375 !$this->applyFilterMethodsToDirectoryItem(
377 $iteratorItem['name'],
378 $iteratorItem['identifier'],
379 $this->getParentFolderIdentifierOfIdentifier($iteratorItem['identifier'])
387 $items[$iteratorItem['identifier']] = $iteratorItem['identifier'];
388 // Decrement item counter to make sure we only return $numberOfItems
389 // we cannot do this earlier in the method (unlike moving the iterator forward) because we only add the
393 } catch (Exception\InvalidPathException
$e) {
400 * 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
401 * directory listings.
403 * @param array $filterMethods The filter methods to use
404 * @param string $itemName
405 * @param string $itemIdentifier
406 * @param string $parentIdentifier
407 * @throws \RuntimeException
410 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, [], $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) {
420 if ($result === false) {
421 throw new \
RuntimeException(
422 'Could not apply file/folder name filter ' . $filter[0] . '::' . $filter[1],
432 * Returns a file inside the specified path
434 * @param string $fileName
435 * @param string $folderIdentifier
436 * @return string File Identifier
438 public function getFileInFolder($fileName, $folderIdentifier)
440 return $this->canonicalizeAndCheckFileIdentifier($folderIdentifier . '/' . $fileName);
444 * Returns a list of files inside the specified path
446 * @param string $folderIdentifier
448 * @param int $numberOfItems
449 * @param bool $recursive
450 * @param array $filenameFilterCallbacks The method callbacks to use for filtering the items
451 * @param string $sort Property name used to sort the items.
452 * Among them may be: '' (empty, no sorting), name,
453 * fileext, size, tstamp and rw.
454 * If a driver does not support the given property, it
455 * should fall back to "name".
456 * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
457 * @return array of FileIdentifiers
459 public function getFilesInFolder($folderIdentifier, $start = 0, $numberOfItems = 0, $recursive = false, array $filenameFilterCallbacks = [], $sort = '', $sortRev = false)
461 return $this->getDirectoryItemList($folderIdentifier, $start, $numberOfItems, $filenameFilterCallbacks, true, false, $recursive, $sort, $sortRev);
465 * Returns the number of files inside the specified path
467 * @param string $folderIdentifier
468 * @param bool $recursive
469 * @param array $filenameFilterCallbacks callbacks for filtering the items
470 * @return int Number of files in folder
472 public function countFilesInFolder($folderIdentifier, $recursive = false, array $filenameFilterCallbacks = [])
474 return count($this->getFilesInFolder($folderIdentifier, 0, 0, $recursive, $filenameFilterCallbacks));
478 * Returns a list of folders inside the specified path
480 * @param string $folderIdentifier
482 * @param int $numberOfItems
483 * @param bool $recursive
484 * @param array $folderNameFilterCallbacks The method callbacks to use for filtering the items
485 * @param string $sort Property name used to sort the items.
486 * Among them may be: '' (empty, no sorting), name,
487 * fileext, size, tstamp and rw.
488 * If a driver does not support the given property, it
489 * should fall back to "name".
490 * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
491 * @return array of Folder Identifier
493 public function getFoldersInFolder($folderIdentifier, $start = 0, $numberOfItems = 0, $recursive = false, array $folderNameFilterCallbacks = [], $sort = '', $sortRev = false)
495 return $this->getDirectoryItemList($folderIdentifier, $start, $numberOfItems, $folderNameFilterCallbacks, false, true, $recursive, $sort, $sortRev);
499 * Returns the number of folders inside the specified path
501 * @param string $folderIdentifier
502 * @param bool $recursive
503 * @param array $folderNameFilterCallbacks callbacks for filtering the items
504 * @return int Number of folders in folder
506 public function countFoldersInFolder($folderIdentifier, $recursive = false, array $folderNameFilterCallbacks = [])
508 return count($this->getFoldersInFolder($folderIdentifier, 0, 0, $recursive, $folderNameFilterCallbacks));
512 * Returns a list with the names of all files and folders in a path, optionally recursive.
514 * @param string $path The absolute path
515 * @param bool $recursive If TRUE, recursively fetches files and folders
516 * @param bool $includeFiles
517 * @param bool $includeDirs
518 * @param string $sort Property name used to sort the items.
519 * Among them may be: '' (empty, no sorting), name,
520 * fileext, size, tstamp and rw.
521 * If a driver does not support the given property, it
522 * should fall back to "name".
523 * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
526 protected function retrieveFileAndFoldersInPath($path, $recursive = false, $includeFiles = true, $includeDirs = true, $sort = '', $sortRev = false)
528 $pathLength = strlen($this->getAbsoluteBasePath());
529 $iteratorMode = \FilesystemIterator
::UNIX_PATHS | \FilesystemIterator
::SKIP_DOTS | \FilesystemIterator
::CURRENT_AS_FILEINFO | \FilesystemIterator
::FOLLOW_SYMLINKS
;
531 $iterator = new \
RecursiveIteratorIterator(
532 new \
RecursiveDirectoryIterator($path, $iteratorMode),
533 \RecursiveIteratorIterator
::SELF_FIRST
,
534 \RecursiveIteratorIterator
::CATCH_GET_CHILD
537 $iterator = new \
RecursiveDirectoryIterator($path, $iteratorMode);
540 $directoryEntries = [];
541 while ($iterator->valid()) {
542 /** @var \SplFileInfo $entry */
543 $entry = $iterator->current();
544 $isFile = $entry->isFile();
545 $isDirectory = $isFile ?
false : $entry->isDir();
547 (!$isFile && !$isDirectory) // skip non-files/non-folders
548 ||
($isFile && !$includeFiles) // skip files if they are excluded
549 ||
($isDirectory && !$includeDirs) // skip directories if they are excluded
550 ||
$entry->getFilename() === '' // skip empty entries
555 $entryIdentifier = '/' . substr($entry->getPathname(), $pathLength);
556 $entryName = PathUtility
::basename($entryIdentifier);
558 $entryIdentifier .= '/';
561 'identifier' => $entryIdentifier,
562 'name' => $entryName,
563 'type' => $isDirectory ?
'dir' : 'file'
565 $directoryEntries[$entryIdentifier] = $entryArray;
568 return $this->sortDirectoryEntries($directoryEntries, $sort, $sortRev);
572 * Sort the directory entries by a certain key
574 * @param array $directoryEntries Array of directory entry arrays from
575 * retrieveFileAndFoldersInPath()
576 * @param string $sort Property name used to sort the items.
577 * Among them may be: '' (empty, no sorting), name,
578 * fileext, size, tstamp and rw.
579 * If a driver does not support the given property, it
580 * should fall back to "name".
581 * @param bool $sortRev TRUE to indicate reverse sorting (last to first)
582 * @return array Sorted entries. Content of the keys is undefined.
584 protected function sortDirectoryEntries($directoryEntries, $sort = '', $sortRev = false)
587 foreach ($directoryEntries as $entryArray) {
588 $dir = pathinfo($entryArray['name'], PATHINFO_DIRNAME
) . '/';
589 $fullPath = $this->getAbsoluteBasePath() . $entryArray['identifier'];
593 if ($entryArray['type'] === 'file') {
594 $sortingKey = $this->getSpecificFileInformation($fullPath, $dir, 'size');
596 // Add a character for a natural order sorting
600 $perms = $this->getPermissions($entryArray['identifier']);
601 $sortingKey = ($perms['r'] ?
'R' : '')
602 . ($perms['w'] ?
'W' : '');
605 $sortingKey = pathinfo($entryArray['name'], PATHINFO_EXTENSION
);
609 if ($entryArray['type'] === 'file') {
610 $sortingKey = $this->getSpecificFileInformation($fullPath, $dir, 'mtime');
612 // Add a character for a natural order sorting
618 $sortingKey = $entryArray['name'];
621 while (isset($entriesToSort[$sortingKey . $i])) {
624 $entriesToSort[$sortingKey . $i] = $entryArray;
626 uksort($entriesToSort, 'strnatcasecmp');
629 $entriesToSort = array_reverse($entriesToSort);
632 return $entriesToSort;
636 * Extracts information about a file from the filesystem.
638 * @param string $filePath The absolute path to the file
639 * @param string $containerPath The relative path to the file's container
640 * @param array $propertiesToExtract array of properties which should be returned, if empty all will be extracted
643 protected function extractFileInformation($filePath, $containerPath, array $propertiesToExtract = [])
645 if (empty($propertiesToExtract)) {
646 $propertiesToExtract = [
647 'size', 'atime', 'mtime', 'ctime', 'mimetype', 'name', 'extension',
648 'identifier', 'identifier_hash', 'storage', 'folder_hash'
651 $fileInformation = [];
652 foreach ($propertiesToExtract as $property) {
653 $fileInformation[$property] = $this->getSpecificFileInformation($filePath, $containerPath, $property);
655 return $fileInformation;
659 * Extracts a specific FileInformation from the FileSystems.
661 * @param string $fileIdentifier
662 * @param string $containerPath
663 * @param string $property
665 * @return bool|int|string
666 * @throws \InvalidArgumentException
668 public function getSpecificFileInformation($fileIdentifier, $containerPath, $property)
670 $identifier = $this->canonicalizeAndCheckFileIdentifier($containerPath . PathUtility
::basename($fileIdentifier));
672 /** @var FileInfo $fileInfo */
673 $fileInfo = GeneralUtility
::makeInstance(FileInfo
::class, $fileIdentifier);
676 return $fileInfo->getSize();
678 return $fileInfo->getATime();
680 return $fileInfo->getMTime();
682 return $fileInfo->getCTime();
684 return PathUtility
::basename($fileIdentifier);
686 return PathUtility
::pathinfo($fileIdentifier, PATHINFO_EXTENSION
);
688 return (string)$fileInfo->getMimeType();
692 return $this->storageUid
;
693 case 'identifier_hash':
694 return $this->hashIdentifier($identifier);
696 return $this->hashIdentifier($this->getParentFolderIdentifierOfIdentifier($identifier));
698 throw new \
InvalidArgumentException(sprintf('The information "%s" is not available.', $property), 1476047422);
703 * Returns the absolute path of the folder this driver operates on.
707 protected function getAbsoluteBasePath()
709 return $this->absoluteBasePath
;
713 * Returns the absolute path of a file or folder.
715 * @param string $fileIdentifier
717 * @throws Exception\InvalidPathException
719 protected function getAbsolutePath($fileIdentifier)
721 $relativeFilePath = ltrim($this->canonicalizeAndCheckFileIdentifier($fileIdentifier), '/');
722 $path = $this->absoluteBasePath
. $relativeFilePath;
727 * Creates a (cryptographic) hash for a file.
729 * @param string $fileIdentifier
730 * @param string $hashAlgorithm The hash algorithm to use
732 * @throws \RuntimeException
733 * @throws \InvalidArgumentException
735 public function hash($fileIdentifier, $hashAlgorithm)
737 if (!in_array($hashAlgorithm, $this->supportedHashAlgorithms
)) {
738 throw new \
InvalidArgumentException('Hash algorithm "' . $hashAlgorithm . '" is not supported.', 1304964032);
740 switch ($hashAlgorithm) {
742 $hash = sha1_file($this->getAbsolutePath($fileIdentifier));
745 $hash = md5_file($this->getAbsolutePath($fileIdentifier));
748 throw new \
RuntimeException('Hash algorithm ' . $hashAlgorithm . ' is not implemented.', 1329644451);
754 * Adds a file from the local server hard disk to a given path in TYPO3s virtual file system.
755 * This assumes that the local file exists, so no further check is done here!
756 * After a successful the original file must not exist anymore.
758 * @param string $localFilePath within public web path
759 * @param string $targetFolderIdentifier
760 * @param string $newFileName optional, if not given original name is used
761 * @param bool $removeOriginal if set the original file will be removed after successful operation
762 * @return string the identifier of the new file
763 * @throws \RuntimeException
764 * @throws \InvalidArgumentException
766 public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = '', $removeOriginal = true)
768 $localFilePath = $this->canonicalizeAndCheckFilePath($localFilePath);
769 // as for the "virtual storage" for backwards-compatibility, this check always fails, as the file probably lies under public web path
770 // thus, it is not checked here
771 // @todo is check in storage
772 if (GeneralUtility
::isFirstPartOfStr($localFilePath, $this->absoluteBasePath
) && $this->storageUid
> 0) {
773 throw new \
InvalidArgumentException('Cannot add a file that is already part of this storage.', 1314778269);
775 $newFileName = $this->sanitizeFileName($newFileName !== '' ?
$newFileName : PathUtility
::basename($localFilePath));
776 $newFileIdentifier = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier) . $newFileName;
777 $targetPath = $this->getAbsolutePath($newFileIdentifier);
779 if ($removeOriginal) {
780 if (is_uploaded_file($localFilePath)) {
781 $result = move_uploaded_file($localFilePath, $targetPath);
783 $result = rename($localFilePath, $targetPath);
786 $result = copy($localFilePath, $targetPath);
788 if ($result === false ||
!file_exists($targetPath)) {
789 throw new \
RuntimeException(
790 'Adding file ' . $localFilePath . ' at ' . $newFileIdentifier . ' failed.',
795 // Change the permissions of the file
796 GeneralUtility
::fixPermissions($targetPath);
797 return $newFileIdentifier;
801 * Checks if a file exists.
803 * @param string $fileIdentifier
807 public function fileExists($fileIdentifier)
809 $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
810 return is_file($absoluteFilePath);
814 * Checks if a file inside a folder exists
816 * @param string $fileName
817 * @param string $folderIdentifier
820 public function fileExistsInFolder($fileName, $folderIdentifier)
822 $identifier = $folderIdentifier . '/' . $fileName;
823 $identifier = $this->canonicalizeAndCheckFileIdentifier($identifier);
824 return $this->fileExists($identifier);
828 * Checks if a folder exists.
830 * @param string $folderIdentifier
834 public function folderExists($folderIdentifier)
836 $absoluteFilePath = $this->getAbsolutePath($folderIdentifier);
837 return is_dir($absoluteFilePath);
841 * Checks if a folder inside a folder exists.
843 * @param string $folderName
844 * @param string $folderIdentifier
847 public function folderExistsInFolder($folderName, $folderIdentifier)
849 $identifier = $folderIdentifier . '/' . $folderName;
850 $identifier = $this->canonicalizeAndCheckFolderIdentifier($identifier);
851 return $this->folderExists($identifier);
855 * Returns the Identifier for a folder within a given folder.
857 * @param string $folderName The name of the target folder
858 * @param string $folderIdentifier
861 public function getFolderInFolder($folderName, $folderIdentifier)
863 $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier . '/' . $folderName);
864 return $folderIdentifier;
868 * Replaces the contents (and file-specific metadata) of a file object with a local file.
870 * @param string $fileIdentifier
871 * @param string $localFilePath
872 * @return bool TRUE if the operation succeeded
873 * @throws \RuntimeException
875 public function replaceFile($fileIdentifier, $localFilePath)
877 $filePath = $this->getAbsolutePath($fileIdentifier);
878 if (is_uploaded_file($localFilePath)) {
879 $result = move_uploaded_file($localFilePath, $filePath);
881 $result = rename($localFilePath, $filePath);
883 GeneralUtility
::fixPermissions($filePath);
884 if ($result === false) {
885 throw new \
RuntimeException('Replacing file ' . $fileIdentifier . ' with ' . $localFilePath . ' failed.', 1315314711);
891 * Copies a file *within* the current storage.
892 * Note that this is only about an intra-storage copy action, where a file is just
893 * copied to another folder in the same storage.
895 * @param string $fileIdentifier
896 * @param string $targetFolderIdentifier
897 * @param string $fileName
898 * @return string the Identifier of the new file
900 public function copyFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $fileName)
902 $sourcePath = $this->getAbsolutePath($fileIdentifier);
903 $newIdentifier = $targetFolderIdentifier . '/' . $fileName;
904 $newIdentifier = $this->canonicalizeAndCheckFileIdentifier($newIdentifier);
906 $absoluteFilePath = $this->getAbsolutePath($newIdentifier);
907 copy($sourcePath, $absoluteFilePath);
908 GeneralUtility
::fixPermissions($absoluteFilePath);
909 return $newIdentifier;
913 * Moves a file *within* the current storage.
914 * Note that this is only about an inner-storage move action, where a file is just
915 * moved to another folder in the same storage.
917 * @param string $fileIdentifier
918 * @param string $targetFolderIdentifier
919 * @param string $newFileName
921 * @throws \RuntimeException
923 public function moveFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $newFileName)
925 $sourcePath = $this->getAbsolutePath($fileIdentifier);
926 $targetIdentifier = $targetFolderIdentifier . '/' . $newFileName;
927 $targetIdentifier = $this->canonicalizeAndCheckFileIdentifier($targetIdentifier);
928 $result = rename($sourcePath, $this->getAbsolutePath($targetIdentifier));
929 if ($result === false) {
930 throw new \
RuntimeException('Moving file ' . $sourcePath . ' to ' . $targetIdentifier . ' failed.', 1315314712);
932 return $targetIdentifier;
936 * Copies a file to a temporary path and returns that path.
938 * @param string $fileIdentifier
939 * @return string The temporary path
940 * @throws \RuntimeException
942 protected function copyFileToTemporaryPath($fileIdentifier)
944 $sourcePath = $this->getAbsolutePath($fileIdentifier);
945 $temporaryPath = $this->getTemporaryPathForFile($fileIdentifier);
946 $result = copy($sourcePath, $temporaryPath);
947 touch($temporaryPath, filemtime($sourcePath));
948 if ($result === false) {
949 throw new \
RuntimeException(
950 'Copying file "' . $fileIdentifier . '" to temporary path "' . $temporaryPath . '" failed.',
954 return $temporaryPath;
958 * Moves a file or folder to the given directory, renaming the source in the process if
959 * a file or folder of the same name already exists in the target path.
961 * @param string $filePath
962 * @param string $recycleDirectory
965 protected function recycleFileOrFolder($filePath, $recycleDirectory)
967 $destinationFile = $recycleDirectory . '/' . PathUtility
::basename($filePath);
968 if (file_exists($destinationFile)) {
969 $timeStamp = \DateTimeImmutable
::createFromFormat('U.u', microtime(true))->format('YmdHisu');
970 $destinationFile = $recycleDirectory . '/' . $timeStamp . '_' . PathUtility
::basename($filePath);
972 $result = rename($filePath, $destinationFile);
973 // Update the mtime for the file, so the recycler garbage collection task knows which files to delete
974 // Using ctime() is not possible there since this is not supported on Windows
976 touch($destinationFile);
982 * Creates a map of old and new file/folder identifiers after renaming or
983 * moving a folder. The old identifier is used as the key, the new one as the value.
985 * @param array $filesAndFolders
986 * @param string $sourceFolderIdentifier
987 * @param string $targetFolderIdentifier
990 * @throws Exception\FileOperationErrorException
992 protected function createIdentifierMap(array $filesAndFolders, $sourceFolderIdentifier, $targetFolderIdentifier)
995 $identifierMap[$sourceFolderIdentifier] = $targetFolderIdentifier;
996 foreach ($filesAndFolders as $oldItem) {
997 if ($oldItem['type'] === 'dir') {
998 $oldIdentifier = $oldItem['identifier'];
999 $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier(
1000 str_replace($sourceFolderIdentifier, $targetFolderIdentifier, $oldItem['identifier'])
1003 $oldIdentifier = $oldItem['identifier'];
1004 $newIdentifier = $this->canonicalizeAndCheckFileIdentifier(
1005 str_replace($sourceFolderIdentifier, $targetFolderIdentifier, $oldItem['identifier'])
1008 if (!file_exists($this->getAbsolutePath($newIdentifier))) {
1009 throw new Exception\
FileOperationErrorException(
1010 sprintf('File "%1$s" was not found (should have been copied/moved from "%2$s").', $newIdentifier, $oldIdentifier),
1014 $identifierMap[$oldIdentifier] = $newIdentifier;
1016 return $identifierMap;
1020 * Folder equivalent to moveFileWithinStorage().
1022 * @param string $sourceFolderIdentifier
1023 * @param string $targetFolderIdentifier
1024 * @param string $newFolderName
1026 * @return array A map of old to new file identifiers
1027 * @throws \RuntimeException
1029 public function moveFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName)
1031 $sourcePath = $this->getAbsolutePath($sourceFolderIdentifier);
1032 $relativeTargetPath = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier . '/' . $newFolderName);
1033 $targetPath = $this->getAbsolutePath($relativeTargetPath);
1034 // get all files and folders we are going to move, to have a map for updating later.
1035 $filesAndFolders = $this->retrieveFileAndFoldersInPath($sourcePath, true);
1036 $result = rename($sourcePath, $targetPath);
1037 if ($result === false) {
1038 throw new \
RuntimeException('Moving folder ' . $sourcePath . ' to ' . $targetPath . ' failed.', 1320711817);
1040 // Create a mapping from old to new identifiers
1041 $identifierMap = $this->createIdentifierMap($filesAndFolders, $sourceFolderIdentifier, $relativeTargetPath);
1042 return $identifierMap;
1046 * Folder equivalent to copyFileWithinStorage().
1048 * @param string $sourceFolderIdentifier
1049 * @param string $targetFolderIdentifier
1050 * @param string $newFolderName
1053 * @throws Exception\FileOperationErrorException
1055 public function copyFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName)
1057 // This target folder path already includes the topmost level, i.e. the folder this method knows as $folderToCopy.
1058 // We can thus rely on this folder being present and just create the subfolder we want to copy to.
1059 $newFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier . '/' . $newFolderName);
1060 $sourceFolderPath = $this->getAbsolutePath($sourceFolderIdentifier);
1061 $targetFolderPath = $this->getAbsolutePath($newFolderIdentifier);
1063 mkdir($targetFolderPath);
1064 /** @var \RecursiveDirectoryIterator $iterator */
1065 $iterator = new \
RecursiveIteratorIterator(
1066 new \
RecursiveDirectoryIterator($sourceFolderPath),
1067 \RecursiveIteratorIterator
::SELF_FIRST
,
1068 \RecursiveIteratorIterator
::CATCH_GET_CHILD
1070 // Rewind the iterator as this is important for some systems e.g. Windows
1071 $iterator->rewind();
1072 while ($iterator->valid()) {
1073 /** @var \RecursiveDirectoryIterator $current */
1074 $current = $iterator->current();
1075 $fileName = $current->getFilename();
1076 $itemSubPath = GeneralUtility
::fixWindowsFilePath($iterator->getSubPathname());
1077 if ($current->isDir() && !($fileName === '..' ||
$fileName === '.')) {
1078 GeneralUtility
::mkdir($targetFolderPath . '/' . $itemSubPath);
1079 } elseif ($current->isFile()) {
1080 $copySourcePath = $sourceFolderPath . '/' . $itemSubPath;
1081 $copyTargetPath = $targetFolderPath . '/' . $itemSubPath;
1082 $result = copy($copySourcePath, $copyTargetPath);
1083 if ($result === false) {
1085 GeneralUtility
::rmdir($targetFolderIdentifier, true);
1086 throw new Exception\
FileOperationErrorException(
1087 'Copying resource "' . $copySourcePath . '" to "' . $copyTargetPath . '" failed.',
1094 GeneralUtility
::fixPermissions($targetFolderPath, true);
1099 * Renames a file in this storage.
1101 * @param string $fileIdentifier
1102 * @param string $newName The target path (including the file name!)
1103 * @return string The identifier of the file after renaming
1104 * @throws Exception\ExistingTargetFileNameException
1105 * @throws \RuntimeException
1107 public function renameFile($fileIdentifier, $newName)
1109 // Makes sure the Path given as parameter is valid
1110 $newName = $this->sanitizeFileName($newName);
1111 $newIdentifier = rtrim(GeneralUtility
::fixWindowsFilePath(PathUtility
::dirname($fileIdentifier)), '/') . '/' . $newName;
1112 $newIdentifier = $this->canonicalizeAndCheckFileIdentifier($newIdentifier);
1113 // The target should not exist already
1114 if ($this->fileExists($newIdentifier)) {
1115 throw new Exception\
ExistingTargetFileNameException(
1116 'The target file "' . $newIdentifier . '" already exists.',
1120 $sourcePath = $this->getAbsolutePath($fileIdentifier);
1121 $targetPath = $this->getAbsolutePath($newIdentifier);
1122 $result = rename($sourcePath, $targetPath);
1123 if ($result === false) {
1124 throw new \
RuntimeException('Renaming file ' . $sourcePath . ' to ' . $targetPath . ' failed.', 1320375115);
1126 return $newIdentifier;
1130 * Renames a folder in this storage.
1132 * @param string $folderIdentifier
1133 * @param string $newName
1134 * @return array A map of old to new file identifiers of all affected files and folders
1135 * @throws \RuntimeException if renaming the folder failed
1137 public function renameFolder($folderIdentifier, $newName)
1139 $folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
1140 $newName = $this->sanitizeFileName($newName);
1142 $newIdentifier = PathUtility
::dirname($folderIdentifier) . '/' . $newName;
1143 $newIdentifier = $this->canonicalizeAndCheckFolderIdentifier($newIdentifier);
1145 $sourcePath = $this->getAbsolutePath($folderIdentifier);
1146 $targetPath = $this->getAbsolutePath($newIdentifier);
1147 // get all files and folders we are going to move, to have a map for updating later.
1148 $filesAndFolders = $this->retrieveFileAndFoldersInPath($sourcePath, true);
1149 $result = rename($sourcePath, $targetPath);
1150 if ($result === false) {
1151 throw new \
RuntimeException(sprintf('Renaming folder "%1$s" to "%2$s" failed."', $sourcePath, $targetPath), 1320375116);
1154 // Create a mapping from old to new identifiers
1155 $identifierMap = $this->createIdentifierMap($filesAndFolders, $folderIdentifier, $newIdentifier);
1156 } catch (\Exception
$e) {
1157 rename($targetPath, $sourcePath);
1158 throw new \
RuntimeException(
1160 'Creating filename mapping after renaming "%1$s" to "%2$s" failed. Reverted rename operation.\\n\\nOriginal error: %3$s"',
1168 return $identifierMap;
1172 * Removes a file from the filesystem. This does not check if the file is
1173 * still used or if it is a bad idea to delete it for some other reason
1174 * this has to be taken care of in the upper layers (e.g. the Storage)!
1176 * @param string $fileIdentifier
1177 * @return bool TRUE if deleting the file succeeded
1178 * @throws \RuntimeException
1180 public function deleteFile($fileIdentifier)
1182 $filePath = $this->getAbsolutePath($fileIdentifier);
1183 $result = unlink($filePath);
1185 if ($result === false) {
1186 throw new \
RuntimeException('Deletion of file ' . $fileIdentifier . ' failed.', 1320855304);
1192 * Removes a folder from this storage.
1194 * @param string $folderIdentifier
1195 * @param bool $deleteRecursively
1197 * @throws Exception\FileOperationErrorException
1198 * @throws Exception\InvalidPathException
1200 public function deleteFolder($folderIdentifier, $deleteRecursively = false)
1202 $folderPath = $this->getAbsolutePath($folderIdentifier);
1203 $recycleDirectory = $this->getRecycleDirectory($folderPath);
1204 if (!empty($recycleDirectory) && $folderPath !== $recycleDirectory) {
1205 $result = $this->recycleFileOrFolder($folderPath, $recycleDirectory);
1207 $result = GeneralUtility
::rmdir($folderPath, $deleteRecursively);
1209 if ($result === false) {
1210 throw new Exception\
FileOperationErrorException(
1211 'Deleting folder "' . $folderIdentifier . '" failed.',
1219 * Checks if a folder contains files and (if supported) other folders.
1221 * @param string $folderIdentifier
1222 * @return bool TRUE if there are no files and folders within $folder
1224 public function isFolderEmpty($folderIdentifier)
1226 $path = $this->getAbsolutePath($folderIdentifier);
1227 $dirHandle = opendir($path);
1228 while ($entry = readdir($dirHandle)) {
1229 if ($entry !== '.' && $entry !== '..') {
1230 closedir($dirHandle);
1234 closedir($dirHandle);
1239 * Returns (a local copy of) a file for processing it. This makes a copy
1240 * first when in writable mode, so if you change the file, you have to update it yourself afterwards.
1242 * @param string $fileIdentifier
1243 * @param bool $writable Set this to FALSE if you only need the file for read operations.
1244 * This might speed up things, e.g. by using a cached local version.
1245 * Never modify the file if you have set this flag!
1246 * @return string The path to the file on the local disk
1248 public function getFileForLocalProcessing($fileIdentifier, $writable = true)
1250 if ($writable === false) {
1251 return $this->getAbsolutePath($fileIdentifier);
1253 return $this->copyFileToTemporaryPath($fileIdentifier);
1257 * Returns the permissions of a file/folder as an array (keys r, w) of boolean flags
1259 * @param string $identifier
1261 * @throws Exception\ResourcePermissionsUnavailableException
1263 public function getPermissions($identifier)
1265 $path = $this->getAbsolutePath($identifier);
1266 $permissionBits = fileperms($path);
1267 if ($permissionBits === false) {
1268 throw new Exception\
ResourcePermissionsUnavailableException('Error while fetching permissions for ' . $path, 1319455097);
1271 'r' => (bool)is_readable($path),
1272 'w' => (bool)is_writable($path)
1277 * Checks if a given identifier is within a container, e.g. if
1278 * a file or folder is within another folder. It will also return
1279 * TRUE if both canonicalized identifiers are equal.
1281 * @param string $folderIdentifier
1282 * @param string $identifier identifier to be checked against $folderIdentifier
1283 * @return bool TRUE if $content is within or matches $folderIdentifier
1285 public function isWithin($folderIdentifier, $identifier)
1287 $folderIdentifier = $this->canonicalizeAndCheckFileIdentifier($folderIdentifier);
1288 $entryIdentifier = $this->canonicalizeAndCheckFileIdentifier($identifier);
1289 if ($folderIdentifier === $entryIdentifier) {
1292 // File identifier canonicalization will not modify a single slash so
1293 // we must not append another slash in that case.
1294 if ($folderIdentifier !== '/') {
1295 $folderIdentifier .= '/';
1297 return GeneralUtility
::isFirstPartOfStr($entryIdentifier, $folderIdentifier);
1301 * Creates a new (empty) file and returns the identifier.
1303 * @param string $fileName
1304 * @param string $parentFolderIdentifier
1306 * @throws \RuntimeException
1308 public function createFile($fileName, $parentFolderIdentifier)
1310 $fileName = $this->sanitizeFileName(ltrim($fileName, '/'));
1311 $parentFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier);
1312 $fileIdentifier = $this->canonicalizeAndCheckFileIdentifier(
1313 $parentFolderIdentifier . $fileName
1315 $absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
1316 $result = touch($absoluteFilePath);
1317 GeneralUtility
::fixPermissions($absoluteFilePath);
1319 if ($result !== true) {
1320 throw new \
RuntimeException('Creating file ' . $fileIdentifier . ' failed.', 1320569854);
1322 return $fileIdentifier;
1326 * Returns the contents of a file. Beware that this requires to load the
1327 * complete file into memory and also may require fetching the file from an
1328 * external location. So this might be an expensive operation (both in terms of
1329 * processing resources and money) for large files.
1331 * @param string $fileIdentifier
1332 * @return string The file contents
1334 public function getFileContents($fileIdentifier)
1336 $filePath = $this->getAbsolutePath($fileIdentifier);
1337 return file_get_contents($filePath);
1341 * Sets the contents of a file to the specified value.
1343 * @param string $fileIdentifier
1344 * @param string $contents
1345 * @return int The number of bytes written to the file
1346 * @throws \RuntimeException if the operation failed
1348 public function setFileContents($fileIdentifier, $contents)
1350 $filePath = $this->getAbsolutePath($fileIdentifier);
1351 $result = file_put_contents($filePath, $contents);
1353 // Make sure later calls to filesize() etc. return correct values.
1354 clearstatcache(true, $filePath);
1356 if ($result === false) {
1357 throw new \
RuntimeException('Setting contents of file "' . $fileIdentifier . '" failed.', 1325419305);
1363 * Returns the role of an item (currently only folders; can later be extended for files as well)
1365 * @param string $folderIdentifier
1368 public function getRole($folderIdentifier)
1370 $name = PathUtility
::basename($folderIdentifier);
1371 $role = $this->mappingFolderNameToRole
[$name] ?? FolderInterface
::ROLE_DEFAULT
;
1376 * Directly output the contents of the file to the output
1377 * buffer. Should not take care of header files or flushing
1378 * buffer before. Will be taken care of by the Storage.
1380 * @param string $identifier
1382 public function dumpFileContents($identifier)
1384 readfile($this->getAbsolutePath($this->canonicalizeAndCheckFileIdentifier($identifier)), 0);
1388 * Stream file using a PSR-7 Response object.
1390 * @param string $identifier
1391 * @param array $properties
1392 * @return ResponseInterface
1394 public function streamFile(string $identifier, array $properties): ResponseInterface
1396 $fileInfo = $this->getFileInfoByIdentifier($identifier, ['name', 'mimetype', 'mtime', 'size']);
1397 $downloadName = $properties['filename_overwrite'] ??
$fileInfo['name'] ??
'';
1398 $mimeType = $properties['mimetype_overwrite'] ??
$fileInfo['mimetype'] ??
'';
1399 $contentDisposition = ($properties['as_download'] ??
false) ?
'attachment' : 'inline';
1401 $filePath = $this->getAbsolutePath($this->canonicalizeAndCheckFileIdentifier($identifier));
1403 return new Response(
1404 new SelfEmittableLazyOpenStream($filePath),
1407 'Content-Disposition' => $contentDisposition . '; filename="' . $downloadName . '"',
1408 'Content-Type' => $mimeType,
1409 'Content-Length' => (string)$fileInfo['size'],
1410 'Last-Modified' => gmdate('D, d M Y H:i:s', $fileInfo['mtime']) . ' GMT',
1411 // Cache-Control header is needed here to solve an issue with browser IE8 and lower
1412 // See for more information: http://support.microsoft.com/kb/323308
1413 'Cache-Control' => '',
1419 * Get the path of the nearest recycler folder of a given $path.
1420 * Return an empty string if there is no recycler folder available.
1422 * @param string $path
1425 protected function getRecycleDirectory($path)
1427 $recyclerSubdirectory = array_search(FolderInterface
::ROLE_RECYCLER
, $this->mappingFolderNameToRole
, true);
1428 if ($recyclerSubdirectory === false) {
1431 $rootDirectory = rtrim($this->getAbsolutePath($this->getRootLevelFolder()), '/');
1432 $searchDirectory = PathUtility
::dirname($path);
1433 // Check if file or folder to be deleted is inside a recycler directory
1434 if ($this->getRole($searchDirectory) === FolderInterface
::ROLE_RECYCLER
) {
1435 $searchDirectory = PathUtility
::dirname($searchDirectory);
1436 // Check if file or folder to be deleted is inside the root recycler
1437 if ($searchDirectory == $rootDirectory) {
1440 $searchDirectory = PathUtility
::dirname($searchDirectory);
1442 // Search for the closest recycler directory
1443 while ($searchDirectory) {
1444 $recycleDirectory = $searchDirectory . '/' . $recyclerSubdirectory;
1445 if (is_dir($recycleDirectory)) {
1446 return $recycleDirectory;
1448 if ($searchDirectory === $rootDirectory) {
1451 $searchDirectory = PathUtility
::dirname($searchDirectory);