1372 lines
58 KiB
PHP
1372 lines
58 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* This file is part of the TYPO3 CMS project.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*
|
|
* The TYPO3 project - inspiring people to share!
|
|
*/
|
|
|
|
namespace TYPO3\CMS\Core\Resource\Driver;
|
|
|
|
use Psr\Http\Message\ResponseInterface;
|
|
use TYPO3\CMS\Core\Charset\CharsetConverter;
|
|
use TYPO3\CMS\Core\Core\Environment;
|
|
use TYPO3\CMS\Core\Http\Response;
|
|
use TYPO3\CMS\Core\Http\SelfEmittableLazyOpenStream;
|
|
use TYPO3\CMS\Core\Resource\Capabilities;
|
|
use TYPO3\CMS\Core\Resource\Exception\ExistingTargetFileNameException;
|
|
use TYPO3\CMS\Core\Resource\Exception\FileOperationErrorException;
|
|
use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
|
|
use TYPO3\CMS\Core\Resource\Exception\InvalidConfigurationException;
|
|
use TYPO3\CMS\Core\Resource\Exception\InvalidFileNameException;
|
|
use TYPO3\CMS\Core\Resource\Exception\InvalidPathException;
|
|
use TYPO3\CMS\Core\Resource\Exception\ResourcePermissionsUnavailableException;
|
|
use TYPO3\CMS\Core\Resource\FolderInterface;
|
|
use TYPO3\CMS\Core\Type\File\FileInfo;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Core\Utility\PathUtility;
|
|
|
|
/**
|
|
* A concrete implementation for a File Driver for the local file system.
|
|
*/
|
|
class LocalDriver extends AbstractHierarchicalFilesystemDriver implements StreamableDriverInterface
|
|
{
|
|
/**
|
|
* @var string
|
|
*/
|
|
public const UNSAFE_FILENAME_CHARACTER_EXPRESSION = '\\x00-\\x2C\\/\\x3A-\\x3F\\x5B-\\x60\\x7B-\\xBF';
|
|
|
|
/**
|
|
* The absolute base path. It always contains a trailing slash.
|
|
*/
|
|
protected string $absoluteBasePath = '/';
|
|
|
|
/**
|
|
* The base URL that points to this driver's storage. As long is this
|
|
* is not set, it is assumed that this folder is not publicly available
|
|
*/
|
|
protected ?string $baseUri = null;
|
|
|
|
/**
|
|
* @var array<non-empty-string, FolderInterface::ROLE_*>
|
|
*/
|
|
protected array $mappingFolderNameToRole = [
|
|
'_recycler_' => FolderInterface::ROLE_RECYCLER,
|
|
'_temp_' => FolderInterface::ROLE_TEMPORARY,
|
|
'user_upload' => FolderInterface::ROLE_USERUPLOAD,
|
|
];
|
|
|
|
public function __construct(array $configuration = [])
|
|
{
|
|
parent::__construct($configuration);
|
|
// The capabilities default of this driver. See Capabilities::CAPABILITY_* constants for possible values
|
|
$this->capabilities = new Capabilities(
|
|
Capabilities::CAPABILITY_BROWSABLE
|
|
| Capabilities::CAPABILITY_PUBLIC
|
|
| Capabilities::CAPABILITY_WRITABLE
|
|
| Capabilities::CAPABILITY_HIERARCHICAL_IDENTIFIERS
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Merges the capabilities from the user of the storage configuration into the actual
|
|
* capabilities of the driver and returns the result.
|
|
*/
|
|
public function mergeConfigurationCapabilities(Capabilities $capabilities): Capabilities
|
|
{
|
|
$this->capabilities->and($capabilities);
|
|
return $this->capabilities;
|
|
}
|
|
|
|
public function processConfiguration(): void
|
|
{
|
|
try {
|
|
$this->absoluteBasePath = $this->calculateBasePath($this->configuration);
|
|
} catch (InvalidConfigurationException $e) {
|
|
// The storage is offline, but the absolute base path requires a "/" at the end.
|
|
$this->absoluteBasePath = '/';
|
|
throw $e;
|
|
}
|
|
$this->determineBaseUrl();
|
|
if ($this->baseUri === null) {
|
|
// remove public flag
|
|
$this->capabilities->removeCapability(Capabilities::CAPABILITY_PUBLIC);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initializes this object. This is called by the storage after the driver
|
|
* has been attached.
|
|
*/
|
|
public function initialize(): void {}
|
|
|
|
/**
|
|
* Determines the base URL for this driver, from the configuration or
|
|
* the public path.
|
|
*/
|
|
protected function determineBaseUrl(): void
|
|
{
|
|
// only calculate baseURI if the storage does not enforce jumpUrl Script
|
|
if ($this->hasCapability(Capabilities::CAPABILITY_PUBLIC)) {
|
|
if (!empty($this->configuration['baseUri'])) {
|
|
$this->baseUri = rtrim($this->configuration['baseUri'], '/') . '/';
|
|
} elseif (str_starts_with($this->absoluteBasePath, Environment::getPublicPath())) {
|
|
// use site-relative URLs
|
|
$temporaryBaseUri = rtrim(PathUtility::stripPathSitePrefix($this->absoluteBasePath), '/');
|
|
if ($temporaryBaseUri !== '') {
|
|
$uriParts = explode('/', $temporaryBaseUri);
|
|
$uriParts = array_map(rawurlencode(...), $uriParts);
|
|
$temporaryBaseUri = implode('/', $uriParts) . '/';
|
|
}
|
|
$this->baseUri = $temporaryBaseUri;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculates the absolute path to this driver's storage location
|
|
*/
|
|
protected function calculateBasePath(array $configuration): string
|
|
{
|
|
if (!array_key_exists('basePath', $configuration) || empty($configuration['basePath'])) {
|
|
throw new InvalidConfigurationException(
|
|
'Configuration must contain base path.',
|
|
1346510477
|
|
);
|
|
}
|
|
|
|
if (!empty($configuration['pathType']) && $configuration['pathType'] === 'relative') {
|
|
$relativeBasePath = $configuration['basePath'];
|
|
$absoluteBasePath = Environment::getPublicPath() . '/' . $relativeBasePath;
|
|
} else {
|
|
$absoluteBasePath = $configuration['basePath'];
|
|
}
|
|
$absoluteBasePath = $this->canonicalizeAndCheckFilePath($absoluteBasePath);
|
|
$absoluteBasePath = rtrim($absoluteBasePath, '/') . '/';
|
|
if (!$this->isAllowedAbsolutePath($absoluteBasePath)) {
|
|
throw new InvalidConfigurationException(
|
|
'Base path "' . $absoluteBasePath . '" is not within the allowed project root path or allowed lockRootPath.',
|
|
1704807715
|
|
);
|
|
}
|
|
if (!is_dir($absoluteBasePath)) {
|
|
throw new InvalidConfigurationException(
|
|
'Base path "' . $absoluteBasePath . '" does not exist or is no directory.',
|
|
1299233097
|
|
);
|
|
}
|
|
return $absoluteBasePath;
|
|
}
|
|
|
|
/**
|
|
* Returns a publicly accessible URL to a file or folder.
|
|
* For the local driver, this will always return a path relative to public web path.
|
|
* For non-public storages, this method returns null.
|
|
*
|
|
* @param non-empty-string $identifier
|
|
* @return string|null NULL if file is missing or deleted, the generated url otherwise
|
|
*/
|
|
public function getPublicUrl(string $identifier): ?string
|
|
{
|
|
$publicUrl = null;
|
|
if ($this->baseUri !== null) {
|
|
$uriParts = explode('/', ltrim($identifier, '/'));
|
|
$uriParts = array_map(rawurlencode(...), $uriParts);
|
|
$identifier = implode('/', $uriParts);
|
|
$publicUrl = $this->baseUri . $identifier;
|
|
}
|
|
return $publicUrl;
|
|
}
|
|
|
|
/**
|
|
* Returns the identifier of the root level folder of the storage.
|
|
*
|
|
* @return non-empty-string
|
|
*/
|
|
public function getRootLevelFolder(): string
|
|
{
|
|
return '/';
|
|
}
|
|
|
|
/**
|
|
* Returns the identifier of the default folder where new files should be put into.
|
|
*
|
|
* @return non-empty-string
|
|
*/
|
|
public function getDefaultFolder(): string
|
|
{
|
|
$identifier = '/user_upload/';
|
|
$createFolder = !$this->folderExists($identifier);
|
|
if ($createFolder === true) {
|
|
$identifier = $this->createFolder('user_upload');
|
|
}
|
|
return $identifier;
|
|
}
|
|
|
|
/**
|
|
* Creates a folder, within a given parent folder.
|
|
* If no parent folder is given, a folder on the root-level will be created
|
|
*
|
|
* @param non-empty-string $newFolderName
|
|
* @return non-empty-string the identifier of the new folder
|
|
*/
|
|
public function createFolder(string $newFolderName, string $parentFolderIdentifier = '', bool $recursive = false): string
|
|
{
|
|
$parentFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier);
|
|
$newFolderName = trim($newFolderName, '/');
|
|
if ($recursive === false) {
|
|
$newFolderName = $this->sanitizeFileName($newFolderName);
|
|
$newIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier . $newFolderName . '/');
|
|
GeneralUtility::mkdir($this->getAbsolutePath($newIdentifier));
|
|
} else {
|
|
$parts = GeneralUtility::trimExplode('/', $newFolderName);
|
|
$parts = array_map($this->sanitizeFileName(...), $parts);
|
|
$newFolderName = implode('/', $parts);
|
|
$newIdentifier = $this->canonicalizeAndCheckFolderIdentifier(
|
|
$parentFolderIdentifier . $newFolderName . '/'
|
|
);
|
|
GeneralUtility::mkdir_deep($this->getAbsolutePath($newIdentifier));
|
|
}
|
|
return $newIdentifier;
|
|
}
|
|
|
|
/**
|
|
* Returns information about a file.
|
|
*
|
|
* @param non-empty-string $fileIdentifier In the case of the LocalDriver, this is the (relative) path to the file.
|
|
* @param list<non-empty-string> $propertiesToExtract Array of properties which should be extracted, if empty all will be extracted
|
|
* @return array<non-empty-string, mixed>
|
|
*/
|
|
public function getFileInfoByIdentifier(string $fileIdentifier, array $propertiesToExtract = []): array
|
|
{
|
|
$absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
|
|
// don't use $this->fileExists() because we need the absolute path to the file anyway, so we can directly
|
|
// use PHP's filesystem method.
|
|
if (!file_exists($absoluteFilePath) || !is_file($absoluteFilePath)) {
|
|
// @todo: This should be turned into a specific exception instead!
|
|
throw new \InvalidArgumentException('File ' . $fileIdentifier . ' does not exist.', 1314516809);
|
|
}
|
|
|
|
$dirPath = PathUtility::dirname($fileIdentifier);
|
|
$dirPath = $this->canonicalizeAndCheckFolderIdentifier($dirPath);
|
|
return $this->extractFileInformation($absoluteFilePath, $dirPath, $propertiesToExtract);
|
|
}
|
|
|
|
/**
|
|
* Returns information about a folder.
|
|
*
|
|
* @param string $folderIdentifier In the case of the LocalDriver, this is the (relative) path to the file.
|
|
* @return array{
|
|
* identifier: non-empty-string,
|
|
* name: string,
|
|
* mtime: int,
|
|
* ctime: int,
|
|
* storage: int,
|
|
* }
|
|
*/
|
|
public function getFolderInfoByIdentifier(string $folderIdentifier): array
|
|
{
|
|
$folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
|
|
|
|
if (!$this->folderExists($folderIdentifier)) {
|
|
throw new FolderDoesNotExistException(
|
|
'Folder "' . $folderIdentifier . '" does not exist.',
|
|
1314516810
|
|
);
|
|
}
|
|
$absolutePath = $this->getAbsolutePath($folderIdentifier);
|
|
return [
|
|
'identifier' => $folderIdentifier,
|
|
'name' => PathUtility::basename($folderIdentifier),
|
|
'mtime' => filemtime($absolutePath),
|
|
'ctime' => filectime($absolutePath),
|
|
'storage' => $this->storageUid,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Returns a string where any character not matching [.a-zA-Z0-9_-] is
|
|
* substituted by '_'
|
|
* Trailing dots are removed and characters are lowercased if using
|
|
* a case insensitive file system.
|
|
*
|
|
* Previously in \TYPO3\CMS\Core\Utility\File\BasicFileUtility::cleanFileName()
|
|
*
|
|
* @param string $fileName Input string, typically the body of a fileName
|
|
* @return non-empty-string Output string with any characters not matching [.a-zA-Z0-9_-] is substituted by '_' and trailing dots removed
|
|
*/
|
|
public function sanitizeFileName(string $fileName): string
|
|
{
|
|
$fileName = \Normalizer::normalize($fileName) ?: $fileName;
|
|
// Handle UTF-8 characters
|
|
if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['UTF8filesystem']) {
|
|
// Allow ".", "-", 0-9, a-z, A-Z and everything beyond U+C0 (latin capital letter a with grave)
|
|
$cleanFileName = (string)preg_replace('/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . ']/u', '_', trim($fileName));
|
|
if (!$this->isCaseSensitiveFileSystem()) {
|
|
$cleanFileName = mb_strtolower($cleanFileName, 'utf-8');
|
|
}
|
|
} else {
|
|
$fileName = GeneralUtility::makeInstance(CharsetConverter::class)->utf8_char_mapping($fileName);
|
|
// Replace unwanted characters with underscores
|
|
$cleanFileName = (string)preg_replace('/[' . self::UNSAFE_FILENAME_CHARACTER_EXPRESSION . '\\xC0-\\xFF]/', '_', trim($fileName));
|
|
if (!$this->isCaseSensitiveFileSystem()) {
|
|
$cleanFileName = strtolower($cleanFileName);
|
|
}
|
|
}
|
|
// Strip trailing dots and return
|
|
$cleanFileName = rtrim($cleanFileName, '.');
|
|
if ($cleanFileName === '') {
|
|
throw new InvalidFileNameException('File name ' . $fileName . ' is invalid.', 1320288991);
|
|
}
|
|
return $cleanFileName;
|
|
}
|
|
|
|
/**
|
|
* Generic wrapper for extracting a list of items from a path.
|
|
*
|
|
* @param int $start The position to start the listing; if not set, start from the beginning
|
|
* @param int $numberOfItems The number of items to list; if set to zero, all items are returned
|
|
* @param array $filterMethods The filter methods used to filter the directory items
|
|
* @param string $sort Property name used to sort the items.
|
|
* Among them may be: '' (empty, no sorting), name,
|
|
* fileext, size, tstamp and rw.
|
|
* If a driver does not support the given property, it
|
|
* should fall back to "name".
|
|
* @param bool $sortRev TRUE to indicate reverse sorting (last to first)
|
|
* @return array<string|int, string> folder identifiers (where key and value are identical, but int-like identifiers
|
|
* will get converted to int array keys)
|
|
*/
|
|
protected function getDirectoryItemList(string $folderIdentifier, int $start, int $numberOfItems, array $filterMethods, bool $includeFiles = true, bool $includeDirs = true, bool $recursive = false, string $sort = '', bool $sortRev = false): array
|
|
{
|
|
$folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
|
|
$realPath = $this->getAbsolutePath($folderIdentifier);
|
|
if (!is_dir($realPath)) {
|
|
throw new \InvalidArgumentException(
|
|
'Cannot list items in directory ' . $folderIdentifier . ' - does not exist or is no directory',
|
|
1314349666
|
|
);
|
|
}
|
|
|
|
$items = $this->retrieveFileAndFoldersInPath($realPath, $recursive, $includeFiles, $includeDirs, $sort, $sortRev);
|
|
$iterator = new \ArrayIterator($items);
|
|
if ($iterator->count() === 0) {
|
|
return [];
|
|
}
|
|
|
|
// $c is the counter for how many items we still have to fetch (-1 is unlimited)
|
|
$c = $numberOfItems > 0 ? $numberOfItems : -1;
|
|
$items = [];
|
|
while ($iterator->valid() && ($numberOfItems === 0 || $c > 0)) {
|
|
// $iteratorItem is the file or folder name
|
|
$iteratorItem = $iterator->current();
|
|
// go on to the next iterator item now as we might skip this one early
|
|
$iterator->next();
|
|
|
|
try {
|
|
if (
|
|
!$this->applyFilterMethodsToDirectoryItem(
|
|
$filterMethods,
|
|
$iteratorItem['name'],
|
|
$iteratorItem['identifier'],
|
|
$this->getParentFolderIdentifierOfIdentifier($iteratorItem['identifier'])
|
|
)
|
|
) {
|
|
continue;
|
|
}
|
|
if ($start > 0) {
|
|
$start--;
|
|
} else {
|
|
// The identifier can also be an int-like string, resulting in int array keys.
|
|
$items[$iteratorItem['identifier']] = $iteratorItem['identifier'];
|
|
// Decrement item counter to make sure we only return $numberOfItems
|
|
// we cannot do this earlier in the method (unlike moving the iterator forward) because we only add the
|
|
// item here
|
|
--$c;
|
|
}
|
|
} catch (InvalidPathException) {
|
|
}
|
|
}
|
|
return $items;
|
|
}
|
|
|
|
/**
|
|
* Applies a set of filter methods to a file name to find out if it should be used or not.
|
|
* This is used by directory listings.
|
|
*
|
|
* @param array $filterMethods The filter methods to use
|
|
*/
|
|
protected function applyFilterMethodsToDirectoryItem(array $filterMethods, string $itemName, string $itemIdentifier, string $parentIdentifier): bool
|
|
{
|
|
foreach ($filterMethods as $filter) {
|
|
if (is_callable($filter)) {
|
|
$result = $filter($itemName, $itemIdentifier, $parentIdentifier, [], $this);
|
|
// We use -1 as the "don't include“ return value, for historic reasons,
|
|
// as call_user_func() used to return FALSE if calling the method failed.
|
|
if ($result === -1) {
|
|
return false;
|
|
}
|
|
if ($result === false) {
|
|
throw new \RuntimeException(
|
|
'Could not apply file/folder name filter ' . $filter[0] . '::' . $filter[1],
|
|
1476046425
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns a file inside the specified path.
|
|
*
|
|
* @param non-empty-string $fileName
|
|
* @param non-empty-string $folderIdentifier
|
|
* @return non-empty-string File Identifier
|
|
*/
|
|
public function getFileInFolder(string $fileName, string $folderIdentifier): string
|
|
{
|
|
return $this->canonicalizeAndCheckFileIdentifier($folderIdentifier . '/' . $fileName);
|
|
}
|
|
|
|
/**
|
|
* Returns a list of files inside the specified path.
|
|
*
|
|
* @param string $folderIdentifier
|
|
* @param int $start
|
|
* @param int $numberOfItems
|
|
* @param bool $recursive
|
|
* @param array $filenameFilterCallbacks The method callbacks to use for filtering the items
|
|
* @param string $sort Property name used to sort the items.
|
|
* Among them may be: '' (empty, no sorting), name,
|
|
* fileext, size, tstamp and rw.
|
|
* If a driver does not support the given property, it
|
|
* should fall back to "name".
|
|
* @param bool $sortRev TRUE to indicate reverse sorting (last to first)
|
|
* @return string[] of FileIdentifiers
|
|
*/
|
|
public function getFilesInFolder(string $folderIdentifier, int $start = 0, int $numberOfItems = 0, bool $recursive = false, array $filenameFilterCallbacks = [], string $sort = '', bool $sortRev = false): array
|
|
{
|
|
return $this->getDirectoryItemList($folderIdentifier, $start, $numberOfItems, $filenameFilterCallbacks, true, false, $recursive, $sort, $sortRev);
|
|
}
|
|
|
|
/**
|
|
* Returns the number of files inside the specified path.
|
|
*
|
|
* @param non-empty-string $folderIdentifier
|
|
* @param list<callable> $filenameFilterCallbacks callbacks for filtering the items
|
|
* @return int<0, max> Number of files in folder
|
|
*/
|
|
public function countFilesInFolder(string $folderIdentifier, bool $recursive = false, array $filenameFilterCallbacks = []): int
|
|
{
|
|
return count($this->getFilesInFolder($folderIdentifier, 0, 0, $recursive, $filenameFilterCallbacks));
|
|
}
|
|
|
|
/**
|
|
* Returns a list of folders inside the specified path.
|
|
*
|
|
* @param string $folderIdentifier
|
|
* @param int $start
|
|
* @param int $numberOfItems
|
|
* @param bool $recursive
|
|
* @param array $folderNameFilterCallbacks The method callbacks to use for filtering the items
|
|
* @param string $sort Property name used to sort the items.
|
|
* Among them may be: '' (empty, no sorting), name,
|
|
* fileext, size, tstamp and rw.
|
|
* If a driver does not support the given property, it
|
|
* should fall back to "name".
|
|
* @param bool $sortRev TRUE to indicate reverse sorting (last to first)
|
|
* @return array<string|int, string> folder identifiers (where key and value are identical, but int-like identifiers
|
|
* will get converted to int array keys)
|
|
*/
|
|
public function getFoldersInFolder(string $folderIdentifier, int $start = 0, int $numberOfItems = 0, bool $recursive = false, array $folderNameFilterCallbacks = [], string $sort = '', bool $sortRev = false): array
|
|
{
|
|
return $this->getDirectoryItemList($folderIdentifier, $start, $numberOfItems, $folderNameFilterCallbacks, false, true, $recursive, $sort, $sortRev);
|
|
}
|
|
|
|
/**
|
|
* Returns the number of folders inside the specified path.
|
|
*
|
|
* @param non-empty-string $folderIdentifier
|
|
* @param list<callable> $folderNameFilterCallbacks callbacks for filtering the items
|
|
* @return int<0, max> Number of folders in folder
|
|
*/
|
|
public function countFoldersInFolder(string $folderIdentifier, bool $recursive = false, array $folderNameFilterCallbacks = []): int
|
|
{
|
|
return count($this->getFoldersInFolder($folderIdentifier, 0, 0, $recursive, $folderNameFilterCallbacks));
|
|
}
|
|
|
|
/**
|
|
* Returns a list with the names of all files and folders in a path, optionally recursive.
|
|
*
|
|
* @param string $path The absolute path
|
|
* @param bool $recursive If TRUE, recursively fetches files and folders
|
|
* @param string $sort Property name used to sort the items.
|
|
* Among them may be: '' (empty, no sorting), name,
|
|
* fileext, size, tstamp and rw.
|
|
* If a driver does not support the given property, it
|
|
* should fall back to "name".
|
|
* @param bool $sortRev TRUE to indicate reverse sorting (last to first)
|
|
*/
|
|
protected function retrieveFileAndFoldersInPath(string $path, bool $recursive = false, bool $includeFiles = true, bool $includeDirs = true, string $sort = '', bool $sortRev = false): array
|
|
{
|
|
$pathLength = strlen($this->getAbsoluteBasePath());
|
|
$iteratorMode = \FilesystemIterator::UNIX_PATHS | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::FOLLOW_SYMLINKS;
|
|
if ($recursive) {
|
|
$iterator = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($path, $iteratorMode),
|
|
\RecursiveIteratorIterator::SELF_FIRST,
|
|
\RecursiveIteratorIterator::CATCH_GET_CHILD
|
|
);
|
|
} else {
|
|
$iterator = new \RecursiveDirectoryIterator($path, $iteratorMode);
|
|
}
|
|
|
|
$directoryEntries = [];
|
|
while ($iterator->valid()) {
|
|
/** @var \SplFileInfo $entry */
|
|
$entry = $iterator->current();
|
|
$isFile = $entry->isFile();
|
|
$isDirectory = !$isFile && $entry->isDir();
|
|
if (
|
|
(!$isFile && !$isDirectory) // skip non-files/non-folders
|
|
|| ($isFile && !$includeFiles) // skip files if they are excluded
|
|
|| ($isDirectory && !$includeDirs) // skip directories if they are excluded
|
|
|| $entry->getFilename() === '' // skip empty entries
|
|
|| !$entry->isReadable() // skip unreadable entries
|
|
) {
|
|
$iterator->next();
|
|
continue;
|
|
}
|
|
$entryIdentifier = '/' . substr($entry->getPathname(), $pathLength);
|
|
$entryName = PathUtility::basename($entryIdentifier);
|
|
if ($isDirectory) {
|
|
$entryIdentifier .= '/';
|
|
}
|
|
$entryArray = [
|
|
'identifier' => $entryIdentifier,
|
|
'name' => $entryName,
|
|
'type' => $isDirectory ? 'dir' : 'file',
|
|
];
|
|
$directoryEntries[$entryIdentifier] = $entryArray;
|
|
$iterator->next();
|
|
}
|
|
return $this->sortDirectoryEntries($directoryEntries, $sort, $sortRev);
|
|
}
|
|
|
|
/**
|
|
* Sort the directory entries by a certain key.
|
|
*
|
|
* @param array $directoryEntries Array of directory entry arrays from retrieveFileAndFoldersInPath()
|
|
* @param string $sort Property name used to sort the items.
|
|
* Among them may be: '' (empty, no sorting), name,
|
|
* fileext, size, tstamp and rw.
|
|
* If a driver does not support the given property, it
|
|
* should fall back to "name".
|
|
* @param bool $sortRev TRUE to indicate reverse sorting (last to first)
|
|
* @return array Sorted entries. Content of the keys is undefined.
|
|
*/
|
|
protected function sortDirectoryEntries(array $directoryEntries, string $sort = '', bool $sortRev = false): array
|
|
{
|
|
if (count($directoryEntries) < 2) {
|
|
return $directoryEntries;
|
|
}
|
|
$entriesToSort = [];
|
|
foreach ($directoryEntries as $entryArray) {
|
|
$dir = pathinfo($entryArray['name'], PATHINFO_DIRNAME) . '/';
|
|
$fullPath = $this->getAbsoluteBasePath() . $entryArray['identifier'];
|
|
switch ($sort) {
|
|
case 'size':
|
|
$sortingKey = '0';
|
|
if ($entryArray['type'] === 'file') {
|
|
$sortingKey = $this->getSpecificFileInformation($fullPath, $dir, 'size');
|
|
}
|
|
// Add a character for a natural order sorting
|
|
$sortingKey .= 's';
|
|
break;
|
|
case 'rw':
|
|
$perms = $this->getPermissions($entryArray['identifier']);
|
|
$sortingKey = ($perms['r'] ? 'R' : '')
|
|
. ($perms['w'] ? 'W' : '');
|
|
break;
|
|
case 'fileext':
|
|
$sortingKey = pathinfo($entryArray['name'], PATHINFO_EXTENSION);
|
|
break;
|
|
case 'tstamp':
|
|
$sortingKey = $this->getSpecificFileInformation($fullPath, $dir, 'mtime');
|
|
// Add a character for a natural order sorting
|
|
$sortingKey .= 't';
|
|
break;
|
|
case 'crdate':
|
|
$sortingKey = $this->getSpecificFileInformation($fullPath, $dir, 'ctime');
|
|
// Add a character for a natural order sorting
|
|
$sortingKey .= 'c';
|
|
break;
|
|
case 'name':
|
|
case 'file':
|
|
default:
|
|
$sortingKey = $entryArray['name'];
|
|
}
|
|
$i = 0;
|
|
while (isset($entriesToSort[$sortingKey . $i])) {
|
|
$i++;
|
|
}
|
|
$entriesToSort[$sortingKey . $i] = $entryArray;
|
|
}
|
|
|
|
$sortMultiplier = $sortRev ? -1 : 1;
|
|
uksort($entriesToSort, static function (string $entryA, string $entryB) use ($sortMultiplier): int {
|
|
return strnatcasecmp($entryA, $entryB) * $sortMultiplier;
|
|
});
|
|
|
|
return $entriesToSort;
|
|
}
|
|
|
|
/**
|
|
* Extracts information about a file from the filesystem.
|
|
*
|
|
* @param string $filePath The absolute path to the file
|
|
* @param string $containerPath The relative path to the file's container
|
|
* @param array $propertiesToExtract array of properties which should be returned, if empty all will be extracted
|
|
*/
|
|
protected function extractFileInformation(string $filePath, string $containerPath, array $propertiesToExtract = []): array
|
|
{
|
|
if (empty($propertiesToExtract)) {
|
|
$propertiesToExtract = [
|
|
'size', 'atime', 'mtime', 'ctime', 'mimetype', 'name', 'extension',
|
|
'identifier', 'identifier_hash', 'storage', 'folder_hash',
|
|
];
|
|
}
|
|
$fileInformation = [];
|
|
foreach ($propertiesToExtract as $property) {
|
|
$fileInformation[$property] = $this->getSpecificFileInformation($filePath, $containerPath, $property);
|
|
}
|
|
return $fileInformation;
|
|
}
|
|
|
|
/**
|
|
* Extracts specific information of a file from the file system.
|
|
*/
|
|
public function getSpecificFileInformation(string $fileIdentifier, string $containerPath, string $property): bool|int|string|null
|
|
{
|
|
$identifier = $this->canonicalizeAndCheckFileIdentifier($containerPath . PathUtility::basename($fileIdentifier));
|
|
|
|
$fileInfo = GeneralUtility::makeInstance(FileInfo::class, $fileIdentifier);
|
|
return match ($property) {
|
|
'size' => $fileInfo->getSize(),
|
|
'atime' => $fileInfo->getATime(),
|
|
'mtime' => $fileInfo->getMTime(),
|
|
'ctime' => $fileInfo->getCTime(),
|
|
'name' => PathUtility::basename($fileIdentifier),
|
|
'extension' => PathUtility::pathinfo($fileIdentifier, PATHINFO_EXTENSION),
|
|
'mimetype' => (string)$fileInfo->getMimeType(),
|
|
'identifier' => $identifier,
|
|
'storage' => $this->storageUid,
|
|
'identifier_hash' => $this->hashIdentifier($identifier),
|
|
'folder_hash' => $this->hashIdentifier($this->getParentFolderIdentifierOfIdentifier($identifier)),
|
|
default => throw new \InvalidArgumentException(
|
|
sprintf('The information "%s" is not available.', $property),
|
|
1476047422
|
|
),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Returns the absolute path of the folder this driver operates on.
|
|
*/
|
|
protected function getAbsoluteBasePath(): string
|
|
{
|
|
return $this->absoluteBasePath;
|
|
}
|
|
|
|
/**
|
|
* Returns the absolute path of a file or folder.
|
|
*/
|
|
protected function getAbsolutePath(string $fileIdentifier): string
|
|
{
|
|
$relativeFilePath = ltrim($this->canonicalizeAndCheckFileIdentifier($fileIdentifier), '/');
|
|
return $this->absoluteBasePath . $relativeFilePath;
|
|
}
|
|
|
|
/**
|
|
* Creates a (cryptographic) hash for a file.
|
|
*
|
|
* @param string $hashAlgorithm The hash algorithm to use
|
|
*/
|
|
public function hash(string $fileIdentifier, string $hashAlgorithm): string
|
|
{
|
|
$hashContext = hash_init($hashAlgorithm);
|
|
hash_update_file($hashContext, $this->getAbsolutePath($fileIdentifier));
|
|
return hash_final($hashContext);
|
|
}
|
|
|
|
/**
|
|
* Adds a file from the local server's hard drive to a given path in TYPO3s storage location.
|
|
* This assumes that the local file exists, so no further check is done here.
|
|
* After a successful "add" operation, the original file must not exist anymore.
|
|
*
|
|
* @param non-empty-string $localFilePath within public web path
|
|
* @param non-empty-string $targetFolderIdentifier
|
|
* @param string $newFileName optional, if not given original name is used
|
|
* @param bool $removeOriginal if set the original file will be removed after successful operation
|
|
* @return non-empty-string the identifier of the new file
|
|
*/
|
|
public function addFile(string $localFilePath, string $targetFolderIdentifier, string $newFileName = '', bool $removeOriginal = true): string
|
|
{
|
|
$localFilePath = $this->canonicalizeAndCheckFilePath($localFilePath);
|
|
// as for the "virtual storage" for backwards-compatibility, this check always fails, as the file probably lies under public web path
|
|
// thus, it is not checked here
|
|
// @todo is check in storage
|
|
if (str_starts_with($localFilePath, $this->absoluteBasePath) && $this->storageUid > 0) {
|
|
throw new \InvalidArgumentException('Cannot add a file that is already part of this storage.', 1314778269);
|
|
}
|
|
$newFileName = $this->sanitizeFileName($newFileName !== '' ? $newFileName : PathUtility::basename($localFilePath));
|
|
$newFileIdentifier = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier) . $newFileName;
|
|
$targetPath = $this->getAbsolutePath($newFileIdentifier);
|
|
|
|
if ($removeOriginal) {
|
|
if (is_uploaded_file($localFilePath)) {
|
|
$result = @move_uploaded_file($localFilePath, $targetPath);
|
|
} else {
|
|
$result = @rename($localFilePath, $targetPath);
|
|
}
|
|
} else {
|
|
$result = @copy($localFilePath, $targetPath);
|
|
}
|
|
if ($result === false || !file_exists($targetPath)) {
|
|
throw new \RuntimeException(
|
|
'Adding file ' . $localFilePath . ' at ' . $newFileIdentifier . ' failed.',
|
|
1476046453
|
|
);
|
|
}
|
|
clearstatcache();
|
|
// Change the permissions of the file
|
|
GeneralUtility::fixPermissions($targetPath);
|
|
return $newFileIdentifier;
|
|
}
|
|
|
|
/**
|
|
* Checks if a file exists on the file system.
|
|
*
|
|
* @param non-empty-string $fileIdentifier
|
|
*/
|
|
public function fileExists(string $fileIdentifier): bool
|
|
{
|
|
$absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
|
|
return is_file($absoluteFilePath);
|
|
}
|
|
|
|
/**
|
|
* Checks if a file inside a folder exists.
|
|
*
|
|
* @param non-empty-string $fileName
|
|
* @param non-empty-string $folderIdentifier
|
|
*/
|
|
public function fileExistsInFolder(string $fileName, string $folderIdentifier): bool
|
|
{
|
|
$identifier = $folderIdentifier . '/' . $fileName;
|
|
$identifier = $this->canonicalizeAndCheckFileIdentifier($identifier);
|
|
return $this->fileExists($identifier);
|
|
}
|
|
|
|
/**
|
|
* Checks if a folder exists.
|
|
*
|
|
* @param non-empty-string $folderIdentifier
|
|
*/
|
|
public function folderExists(string $folderIdentifier): bool
|
|
{
|
|
$absoluteFilePath = $this->getAbsolutePath($folderIdentifier);
|
|
return is_dir($absoluteFilePath);
|
|
}
|
|
|
|
/**
|
|
* Checks if a folder inside a folder exists.
|
|
*
|
|
* @param non-empty-string $folderName
|
|
* @param non-empty-string $folderIdentifier
|
|
*/
|
|
public function folderExistsInFolder(string $folderName, string $folderIdentifier): bool
|
|
{
|
|
$identifier = $folderIdentifier . '/' . $folderName;
|
|
$identifier = $this->canonicalizeAndCheckFolderIdentifier($identifier);
|
|
return $this->folderExists($identifier);
|
|
}
|
|
|
|
/**
|
|
* Returns the identifier for a folder within a given folder.
|
|
*
|
|
* @param non-empty-string $folderName The name of the target folder
|
|
* @param non-empty-string $folderIdentifier
|
|
* @return non-empty-string
|
|
*/
|
|
public function getFolderInFolder(string $folderName, string $folderIdentifier): string
|
|
{
|
|
return $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier . '/' . $folderName);
|
|
}
|
|
|
|
/**
|
|
* Replaces the contents (and file-specific metadata) of a file with another file from the server's hard disk.
|
|
*
|
|
* @param non-empty-string $fileIdentifier
|
|
* @param non-empty-string $localFilePath
|
|
*/
|
|
public function replaceFile(string $fileIdentifier, string $localFilePath): bool
|
|
{
|
|
$filePath = $this->getAbsolutePath($fileIdentifier);
|
|
if (is_uploaded_file($localFilePath)) {
|
|
$result = @move_uploaded_file($localFilePath, $filePath);
|
|
} else {
|
|
$result = @rename($localFilePath, $filePath);
|
|
}
|
|
GeneralUtility::fixPermissions($filePath);
|
|
if ($result === false) {
|
|
throw new \RuntimeException('Replacing file ' . $fileIdentifier . ' with ' . $localFilePath . ' failed.', 1315314711);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Copies a file *within* the current storage.
|
|
* The responsibility of this method in the Driver is only about an intra-storage copy action,
|
|
* where a file is just copied to another folder in the same storage.
|
|
*
|
|
* @param non-empty-string $fileIdentifier
|
|
* @param non-empty-string $targetFolderIdentifier
|
|
* @param non-empty-string $fileName
|
|
* @return non-empty-string the identifier of the new file
|
|
*/
|
|
public function copyFileWithinStorage(string $fileIdentifier, string $targetFolderIdentifier, string $fileName): string
|
|
{
|
|
$sourcePath = $this->getAbsolutePath($fileIdentifier);
|
|
$newIdentifier = $targetFolderIdentifier . '/' . $fileName;
|
|
$newIdentifier = $this->canonicalizeAndCheckFileIdentifier($newIdentifier);
|
|
|
|
$absoluteFilePath = $this->getAbsolutePath($newIdentifier);
|
|
@copy($sourcePath, $absoluteFilePath);
|
|
GeneralUtility::fixPermissions($absoluteFilePath);
|
|
return $newIdentifier;
|
|
}
|
|
|
|
/**
|
|
* Moves a file *within* the current storage.
|
|
* The responsibility of this method in the Driver is only about an intra-storage move action,
|
|
* where a file is just moved to another folder in the same storage.
|
|
*
|
|
* @param non-empty-string $fileIdentifier
|
|
* @param non-empty-string $targetFolderIdentifier
|
|
* @param non-empty-string $newFileName
|
|
* @return non-empty-string
|
|
*/
|
|
public function moveFileWithinStorage(string $fileIdentifier, string $targetFolderIdentifier, string $newFileName): string
|
|
{
|
|
$sourcePath = $this->getAbsolutePath($fileIdentifier);
|
|
$targetIdentifier = $targetFolderIdentifier . '/' . $newFileName;
|
|
$targetIdentifier = $this->canonicalizeAndCheckFileIdentifier($targetIdentifier);
|
|
$result = @rename($sourcePath, $this->getAbsolutePath($targetIdentifier));
|
|
if ($result === false) {
|
|
throw new \RuntimeException('Moving file ' . $sourcePath . ' to ' . $targetIdentifier . ' failed.', 1315314712);
|
|
}
|
|
return $targetIdentifier;
|
|
}
|
|
|
|
/**
|
|
* Copies a file to a temporary path and returns that path.
|
|
*/
|
|
protected function copyFileToTemporaryPath(string $fileIdentifier): string
|
|
{
|
|
$sourcePath = $this->getAbsolutePath($fileIdentifier);
|
|
$temporaryPath = $this->getTemporaryPathForFile($fileIdentifier);
|
|
$result = @copy($sourcePath, $temporaryPath);
|
|
if ($result === false) {
|
|
throw new \RuntimeException(
|
|
'Copying file "' . $fileIdentifier . '" to temporary path "' . $temporaryPath . '" failed.',
|
|
1320577649
|
|
);
|
|
}
|
|
@touch($temporaryPath, (int)filemtime($sourcePath));
|
|
return $temporaryPath;
|
|
}
|
|
|
|
/**
|
|
* Moves a file or folder to the given directory, renaming the source in the process if
|
|
* a file or folder of the same name already exists in the target path.
|
|
*/
|
|
protected function recycleFileOrFolder(string $filePath, string $recycleDirectory): bool
|
|
{
|
|
$destinationFile = $recycleDirectory . '/' . PathUtility::basename($filePath);
|
|
if (file_exists($destinationFile)) {
|
|
$timeStamp = \DateTimeImmutable::createFromFormat('U.u', (string)microtime(true))->format('YmdHisu');
|
|
$destinationFile = $recycleDirectory . '/' . $timeStamp . '_' . PathUtility::basename($filePath);
|
|
}
|
|
$result = @rename($filePath, $destinationFile);
|
|
// Update the mtime for the file, so the recycler garbage collection task knows which files to delete
|
|
// Using ctime() is not possible there since this is not supported on Windows
|
|
if ($result) {
|
|
@touch($destinationFile);
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Creates a map of old and new file/folder identifiers after renaming or
|
|
* moving a folder. The old identifier is used as the key, the new one as the value.
|
|
* @return array<non-empty-string, non-empty-string>
|
|
*/
|
|
protected function createIdentifierMap(array $filesAndFolders, string $sourceFolderIdentifier, string $targetFolderIdentifier): array
|
|
{
|
|
$identifierMap = [];
|
|
$identifierMap[$sourceFolderIdentifier] = $targetFolderIdentifier;
|
|
foreach ($filesAndFolders as $oldItem) {
|
|
$oldIdentifier = $oldItem['identifier'];
|
|
if ($oldItem['type'] === 'dir') {
|
|
$newIdentifier = $this->canonicalizeAndCheckFolderIdentifier(
|
|
str_replace($sourceFolderIdentifier, $targetFolderIdentifier, $oldItem['identifier'])
|
|
);
|
|
} else {
|
|
$newIdentifier = $this->canonicalizeAndCheckFileIdentifier(
|
|
str_replace($sourceFolderIdentifier, $targetFolderIdentifier, $oldItem['identifier'])
|
|
);
|
|
}
|
|
if (!file_exists($this->getAbsolutePath($newIdentifier))) {
|
|
throw new FileOperationErrorException(
|
|
sprintf('File "%1$s" was not found (should have been copied/moved from "%2$s").', $newIdentifier, $oldIdentifier),
|
|
1330119453
|
|
);
|
|
}
|
|
$identifierMap[$oldIdentifier] = $newIdentifier;
|
|
}
|
|
return $identifierMap;
|
|
}
|
|
|
|
/**
|
|
* Folder equivalent to moveFileWithinStorage().
|
|
*
|
|
* @param non-empty-string $sourceFolderIdentifier
|
|
* @param non-empty-string $targetFolderIdentifier
|
|
* @param non-empty-string $newFolderName
|
|
* @return array<non-empty-string, non-empty-string> A map of old to new file identifiers
|
|
*/
|
|
public function moveFolderWithinStorage(string $sourceFolderIdentifier, string $targetFolderIdentifier, string $newFolderName): array
|
|
{
|
|
$sourcePath = $this->getAbsolutePath($sourceFolderIdentifier);
|
|
$relativeTargetPath = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier . '/' . $newFolderName);
|
|
$targetPath = $this->getAbsolutePath($relativeTargetPath);
|
|
// get all files and folders we are going to move, to have a map for updating later.
|
|
$filesAndFolders = $this->retrieveFileAndFoldersInPath($sourcePath, true);
|
|
$result = @rename($sourcePath, $targetPath);
|
|
if ($result === false) {
|
|
throw new \RuntimeException('Moving folder ' . $sourcePath . ' to ' . $targetPath . ' failed.', 1320711817);
|
|
}
|
|
// Create a mapping from old to new identifiers
|
|
return $this->createIdentifierMap($filesAndFolders, $sourceFolderIdentifier, $relativeTargetPath);
|
|
}
|
|
|
|
/**
|
|
* Folder equivalent to copyFileWithinStorage().
|
|
*
|
|
* @param non-empty-string $sourceFolderIdentifier
|
|
* @param non-empty-string $targetFolderIdentifier
|
|
* @param non-empty-string $newFolderName
|
|
* @return true
|
|
*/
|
|
public function copyFolderWithinStorage(string $sourceFolderIdentifier, string $targetFolderIdentifier, string $newFolderName): bool
|
|
{
|
|
// This target folder path already includes the topmost level, i.e. the folder this method knows as $folderToCopy.
|
|
// We can thus rely on this folder being present and just create the subfolder we want to copy to.
|
|
$newFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($targetFolderIdentifier . '/' . $newFolderName);
|
|
$sourceFolderPath = $this->getAbsolutePath($sourceFolderIdentifier);
|
|
$targetFolderPath = $this->getAbsolutePath($newFolderIdentifier);
|
|
|
|
GeneralUtility::mkdir($targetFolderPath);
|
|
$iterator = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($sourceFolderPath),
|
|
\RecursiveIteratorIterator::SELF_FIRST,
|
|
\RecursiveIteratorIterator::CATCH_GET_CHILD
|
|
);
|
|
// Rewind the iterator as this is important for some systems e.g. Windows
|
|
$iterator->rewind();
|
|
while ($iterator->valid()) {
|
|
/** @var \RecursiveDirectoryIterator $current */
|
|
$current = $iterator->current();
|
|
$fileName = $current->getFilename();
|
|
$itemSubPath = GeneralUtility::fixWindowsFilePath((string)$iterator->getSubPathname());
|
|
if ($current->isDir() && !($fileName === '..' || $fileName === '.')) {
|
|
GeneralUtility::mkdir($targetFolderPath . '/' . $itemSubPath);
|
|
} elseif ($current->isFile()) {
|
|
$copySourcePath = $sourceFolderPath . '/' . $itemSubPath;
|
|
$copyTargetPath = $targetFolderPath . '/' . $itemSubPath;
|
|
$result = @copy($copySourcePath, $copyTargetPath);
|
|
if ($result === false) {
|
|
// rollback
|
|
GeneralUtility::rmdir($targetFolderIdentifier, true);
|
|
throw new FileOperationErrorException(
|
|
'Copying resource "' . $copySourcePath . '" to "' . $copyTargetPath . '" failed.',
|
|
1330119452
|
|
);
|
|
}
|
|
}
|
|
$iterator->next();
|
|
}
|
|
GeneralUtility::fixPermissions($targetFolderPath, true);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Renames a file in this storage.
|
|
*
|
|
* @param non-empty-string $fileIdentifier
|
|
* @param non-empty-string $newName The target path (including the file name!)
|
|
* @return non-empty-string The identifier of the file after renaming
|
|
*/
|
|
public function renameFile(string $fileIdentifier, string $newName): string
|
|
{
|
|
// Makes sure the Path given as parameter is valid
|
|
$newName = $this->sanitizeFileName($newName);
|
|
$newIdentifier = rtrim(GeneralUtility::fixWindowsFilePath(PathUtility::dirname($fileIdentifier)), '/') . '/' . $newName;
|
|
$newIdentifier = $this->canonicalizeAndCheckFileIdentifier($newIdentifier);
|
|
// The target should not exist already
|
|
if ($this->fileExists($newIdentifier)) {
|
|
throw new ExistingTargetFileNameException(
|
|
'The target file "' . $newIdentifier . '" already exists.',
|
|
1320291063
|
|
);
|
|
}
|
|
$sourcePath = $this->getAbsolutePath($fileIdentifier);
|
|
$targetPath = $this->getAbsolutePath($newIdentifier);
|
|
$result = @rename($sourcePath, $targetPath);
|
|
if ($result === false) {
|
|
throw new \RuntimeException('Renaming file ' . $sourcePath . ' to ' . $targetPath . ' failed.', 1320375115);
|
|
}
|
|
return $newIdentifier;
|
|
}
|
|
|
|
/**
|
|
* Renames a folder in this storage.
|
|
*
|
|
* @param non-empty-string $folderIdentifier
|
|
* @param non-empty-string $newName
|
|
* @return array<string, string> A map of old to new file identifiers of all affected files and folders
|
|
* @throws \RuntimeException if renaming the folder failed
|
|
*/
|
|
public function renameFolder(string $folderIdentifier, string $newName): array
|
|
{
|
|
$folderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($folderIdentifier);
|
|
$newName = $this->sanitizeFileName($newName);
|
|
|
|
$newIdentifier = PathUtility::dirname($folderIdentifier) . '/' . $newName;
|
|
$newIdentifier = $this->canonicalizeAndCheckFolderIdentifier($newIdentifier);
|
|
|
|
$sourcePath = $this->getAbsolutePath($folderIdentifier);
|
|
$targetPath = $this->getAbsolutePath($newIdentifier);
|
|
// get all files and folders we are going to move, to have a map for updating later.
|
|
$filesAndFolders = $this->retrieveFileAndFoldersInPath($sourcePath, true);
|
|
$result = @rename($sourcePath, $targetPath);
|
|
if ($result === false) {
|
|
throw new \RuntimeException(sprintf('Renaming folder "%1$s" to "%2$s" failed."', $sourcePath, $targetPath), 1320375116);
|
|
}
|
|
try {
|
|
// Create a mapping from old to new identifiers
|
|
$identifierMap = $this->createIdentifierMap($filesAndFolders, $folderIdentifier, $newIdentifier);
|
|
} catch (\Exception $e) {
|
|
@rename($targetPath, $sourcePath);
|
|
throw new \RuntimeException(
|
|
sprintf(
|
|
'Creating filename mapping after renaming "%1$s" to "%2$s" failed. Reverted rename operation.\\n\\nOriginal error: %3$s"',
|
|
$sourcePath,
|
|
$targetPath,
|
|
$e->getMessage()
|
|
),
|
|
1334160746
|
|
);
|
|
}
|
|
return $identifierMap;
|
|
}
|
|
|
|
/**
|
|
* Removes a file from the filesystem. This does not check if the file is
|
|
* still used or if it is a bad idea to delete it for some other reason
|
|
* this has to be taken care of in the upper layers (e.g. the ResourceStorage).
|
|
*
|
|
* @param non-empty-string $fileIdentifier
|
|
*/
|
|
public function deleteFile(string $fileIdentifier): bool
|
|
{
|
|
$filePath = $this->getAbsolutePath($fileIdentifier);
|
|
$result = @unlink($filePath);
|
|
|
|
if ($result === false) {
|
|
throw new \RuntimeException('Deletion of file ' . $fileIdentifier . ' failed.', 1320855304);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Removes a folder from the hard drive.
|
|
*
|
|
* @param non-empty-string $folderIdentifier
|
|
*/
|
|
public function deleteFolder(string $folderIdentifier, bool $deleteRecursively = false): bool
|
|
{
|
|
$folderPath = $this->getAbsolutePath($folderIdentifier);
|
|
$recycleDirectory = $this->getRecycleDirectory($folderPath);
|
|
if (!empty($recycleDirectory) && $folderPath !== $recycleDirectory) {
|
|
$result = $this->recycleFileOrFolder($folderPath, $recycleDirectory);
|
|
} else {
|
|
$result = GeneralUtility::rmdir($folderPath, $deleteRecursively);
|
|
}
|
|
if ($result === false) {
|
|
throw new FileOperationErrorException(
|
|
'Deleting folder "' . $folderIdentifier . '" failed.',
|
|
1330119451
|
|
);
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Checks if a folder contains files and (if supported) other folders.
|
|
*
|
|
* @param non-empty-string $folderIdentifier
|
|
* @return bool TRUE if there are no files and folders within $folder
|
|
*/
|
|
public function isFolderEmpty(string $folderIdentifier): bool
|
|
{
|
|
$path = $this->getAbsolutePath($folderIdentifier);
|
|
$dirHandle = opendir($path);
|
|
if ($dirHandle === false) {
|
|
return true;
|
|
}
|
|
while ($entry = readdir($dirHandle)) {
|
|
if ($entry !== '.' && $entry !== '..') {
|
|
closedir($dirHandle);
|
|
return false;
|
|
}
|
|
}
|
|
closedir($dirHandle);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns (a local copy of) a file for further processing. This makes a copy
|
|
* first when in writable mode, so if you change the file, you have to update it yourself afterward.
|
|
*
|
|
* @param non-empty-string $fileIdentifier
|
|
* @param bool $writable Set this to FALSE if you only need the file for read operations.
|
|
* This might speed up things, e.g. by using a cached local version.
|
|
* Never modify the file if you have set this flag!
|
|
* @return non-empty-string The path to the file on the local disk
|
|
*/
|
|
public function getFileForLocalProcessing(string $fileIdentifier, bool $writable = true): string
|
|
{
|
|
if ($writable === false) {
|
|
return $this->getAbsolutePath($fileIdentifier);
|
|
}
|
|
return $this->copyFileToTemporaryPath($fileIdentifier);
|
|
}
|
|
|
|
/**
|
|
* Returns the permissions of a file/folder as an array (keys r, w) of boolean flags.
|
|
*
|
|
* @param non-empty-string $identifier
|
|
* @return array{r: bool, w: bool}
|
|
*/
|
|
public function getPermissions(string $identifier): array
|
|
{
|
|
$path = $this->getAbsolutePath($identifier);
|
|
$permissionBits = fileperms($path);
|
|
if ($permissionBits === false) {
|
|
throw new ResourcePermissionsUnavailableException('Error while fetching permissions for ' . $path, 1319455097);
|
|
}
|
|
return [
|
|
'r' => is_readable($path),
|
|
'w' => is_writable($path),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Checks if a given identifier is within a container, e.g. if
|
|
* a file or folder is within another folder. It will also return
|
|
* TRUE if both canonical identifiers are equal.
|
|
*
|
|
* @param non-empty-string $folderIdentifier
|
|
* @param non-empty-string $identifier identifier to be checked against $folderIdentifier
|
|
* @return bool TRUE if $content is within or matches $folderIdentifier
|
|
*/
|
|
public function isWithin(string $folderIdentifier, string $identifier): bool
|
|
{
|
|
$folderIdentifier = $this->canonicalizeAndCheckFileIdentifier($folderIdentifier);
|
|
$entryIdentifier = $this->canonicalizeAndCheckFileIdentifier($identifier);
|
|
if ($folderIdentifier === $entryIdentifier) {
|
|
return true;
|
|
}
|
|
// File identifier canonicalization will not modify a single slash so
|
|
// we must not append another slash in that case.
|
|
if ($folderIdentifier !== '/') {
|
|
$folderIdentifier .= '/';
|
|
}
|
|
return str_starts_with($entryIdentifier, $folderIdentifier);
|
|
}
|
|
|
|
/**
|
|
* Creates a new (empty) file and returns the identifier.
|
|
*
|
|
* @param non-empty-string $fileName
|
|
* @param non-empty-string $parentFolderIdentifier
|
|
* @return non-empty-string
|
|
*/
|
|
public function createFile(string $fileName, string $parentFolderIdentifier): string
|
|
{
|
|
$fileName = $this->sanitizeFileName(ltrim($fileName, '/'));
|
|
$parentFolderIdentifier = $this->canonicalizeAndCheckFolderIdentifier($parentFolderIdentifier);
|
|
$fileIdentifier = $this->canonicalizeAndCheckFileIdentifier(
|
|
$parentFolderIdentifier . $fileName
|
|
);
|
|
$absoluteFilePath = $this->getAbsolutePath($fileIdentifier);
|
|
$result = @touch($absoluteFilePath);
|
|
if ($result !== true) {
|
|
throw new \RuntimeException('Creating file ' . $fileIdentifier . ' failed.', 1320569854);
|
|
}
|
|
GeneralUtility::fixPermissions($absoluteFilePath);
|
|
clearstatcache();
|
|
return $fileIdentifier;
|
|
}
|
|
|
|
/**
|
|
* Returns the contents of a file. Beware that this requires to load the
|
|
* complete file into memory and also may require fetching the file from an
|
|
* external location. So this might be an expensive operation (both in terms of
|
|
* processing resources and money) for large files.
|
|
*
|
|
* @param non-empty-string $fileIdentifier
|
|
* @return string The file contents if file exists, otherwise an empty string
|
|
*/
|
|
public function getFileContents(string $fileIdentifier): string
|
|
{
|
|
$filePath = $this->getAbsolutePath($fileIdentifier);
|
|
return is_readable($filePath) ? (string)file_get_contents($filePath) : '';
|
|
}
|
|
|
|
/**
|
|
* Sets the contents of a file to the specified value.
|
|
*
|
|
* @param non-empty-string $fileIdentifier
|
|
* @return int<0, max> The number of bytes written to the file
|
|
* @throws \RuntimeException if the operation failed
|
|
*/
|
|
public function setFileContents(string $fileIdentifier, string $contents): int
|
|
{
|
|
$filePath = $this->getAbsolutePath($fileIdentifier);
|
|
$result = file_put_contents($filePath, $contents);
|
|
|
|
// Make sure later calls to filesize() etc. return correct values.
|
|
clearstatcache(true, $filePath);
|
|
|
|
if ($result === false) {
|
|
throw new \RuntimeException('Setting contents of file "' . $fileIdentifier . '" failed.', 1325419305);
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Returns the role of an item. This is currently only implemented for folder,
|
|
* but could be extended to files as well.
|
|
*/
|
|
public function getRole(string $folderIdentifier): string
|
|
{
|
|
$name = PathUtility::basename($folderIdentifier);
|
|
return $this->mappingFolderNameToRole[$name] ?? FolderInterface::ROLE_DEFAULT;
|
|
}
|
|
|
|
/**
|
|
* Directly output the contents of the file to the output buffer.
|
|
* Should not take care of header files or flushing buffer before. Will be taken care of by the Storage.
|
|
*
|
|
* @param non-empty-string $identifier
|
|
*/
|
|
public function dumpFileContents(string $identifier): void
|
|
{
|
|
readfile($this->getAbsolutePath($this->canonicalizeAndCheckFileIdentifier($identifier)));
|
|
}
|
|
|
|
/**
|
|
* Stream file using a PSR-7 Response object.
|
|
*/
|
|
public function streamFile(string $identifier, array $properties): ResponseInterface
|
|
{
|
|
$fileInfo = $this->getFileInfoByIdentifier($identifier, ['name', 'mimetype', 'mtime', 'size']);
|
|
$downloadName = $properties['filename_overwrite'] ?? $fileInfo['name'] ?? '';
|
|
$mimeType = $properties['mimetype_overwrite'] ?? $fileInfo['mimetype'] ?? '';
|
|
$contentDisposition = ($properties['as_download'] ?? false) ? 'attachment' : 'inline';
|
|
|
|
$filePath = $this->getAbsolutePath($this->canonicalizeAndCheckFileIdentifier($identifier));
|
|
|
|
return new Response(
|
|
new SelfEmittableLazyOpenStream($filePath),
|
|
200,
|
|
[
|
|
'Content-Disposition' => $contentDisposition . '; filename="' . $downloadName . '"',
|
|
'Content-Type' => $mimeType,
|
|
'Content-Length' => (string)$fileInfo['size'],
|
|
'Last-Modified' => gmdate('D, d M Y H:i:s', $fileInfo['mtime']) . ' GMT',
|
|
// Cache-Control header is needed here to solve an issue with browser IE8 and lower
|
|
// See for more information: http://support.microsoft.com/kb/323308
|
|
'Cache-Control' => '',
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get the path of the nearest recycler folder of a given path.
|
|
* Return an empty string if there is no recycler folder available in the base path.
|
|
*/
|
|
protected function getRecycleDirectory(string $path): string
|
|
{
|
|
$recyclerSubdirectory = array_search(FolderInterface::ROLE_RECYCLER, $this->mappingFolderNameToRole, true);
|
|
if ($recyclerSubdirectory === false) {
|
|
return '';
|
|
}
|
|
$rootDirectory = rtrim($this->getAbsolutePath($this->getRootLevelFolder()), '/');
|
|
$searchDirectory = PathUtility::dirname($path);
|
|
// Check if file or folder to be deleted is inside a recycler directory
|
|
if ($this->getRole($searchDirectory) === FolderInterface::ROLE_RECYCLER) {
|
|
$searchDirectory = PathUtility::dirname($searchDirectory);
|
|
// Check if file or folder to be deleted is inside the root recycler
|
|
if ($searchDirectory == $rootDirectory) {
|
|
return '';
|
|
}
|
|
$searchDirectory = PathUtility::dirname($searchDirectory);
|
|
}
|
|
// Search for the closest recycler directory
|
|
while ($searchDirectory) {
|
|
$recycleDirectory = $searchDirectory . '/' . $recyclerSubdirectory;
|
|
if (is_dir($recycleDirectory)) {
|
|
return $recycleDirectory;
|
|
}
|
|
if ($searchDirectory === $rootDirectory) {
|
|
return '';
|
|
}
|
|
$searchDirectory = PathUtility::dirname($searchDirectory);
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Wrapper for `GeneralUtility::isAllowedAbsPath`, which implicitly invokes
|
|
* `GeneralUtility::validPathStr` (like in `parent::isPathValid`).
|
|
*/
|
|
protected function isAllowedAbsolutePath(string $path): bool
|
|
{
|
|
return GeneralUtility::isAllowedAbsPath($path);
|
|
}
|
|
}
|