diff --git a/Classes/Driver/AmazonS3Driver.php b/Classes/Driver/AmazonS3Driver.php index fd92f83f..4d5c7abf 100644 --- a/Classes/Driver/AmazonS3Driver.php +++ b/Classes/Driver/AmazonS3Driver.php @@ -80,6 +80,14 @@ class AmazonS3Driver extends AbstractHierarchicalFilesystemDriver implements Str */ protected $baseUrl = ''; + /** + * Folder that is used as root folder. + * Must be empty or have a trailing slash. + * + * @var string + */ + protected $baseFolder = ''; + /** * Stream wrapper protocol: Will be set in the constructor * @@ -219,8 +227,10 @@ public function processConfiguration() public function initialize() { $this->initializeBaseUrl() + ->initializeBaseFolder() ->initializeSettings() ->initializeClient(); + $this->resetRequestCache(); // Test connection if we are in the edit view of this storage if ( $this->compatibilityService->isBackend() @@ -238,7 +248,7 @@ public function getPublicUrl($identifier) { $uriParts = GeneralUtility::trimExplode('/', ltrim($identifier, '/'), true); $uriParts = array_map('rawurlencode', $uriParts); - return $this->baseUrl . '/' . implode('/', $uriParts); + return $this->baseUrl . '/' . $this->addBaseFolder(implode('/', $uriParts)); } /** @@ -394,14 +404,14 @@ public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = if (filesize($localFilePath) === 0) { // Multipart uploader would fail to upload empty files $this->s3Client->upload( $this->configuration['bucket'], - $targetIdentifier, + $this->addBaseFolder($targetIdentifier), '' ); } else { $multipartUploadAdapter = GeneralUtility::makeInstance(MultipartUploaderAdapter::class, $this->s3Client); $multipartUploadAdapter->upload( $localFilePath, - $targetIdentifier, + $this->addBaseFolder($targetIdentifier), $this->configuration['bucket'], $this->getCacheControl($targetIdentifier) ); @@ -524,7 +534,7 @@ public function getFileForLocalProcessing($fileIdentifier, $writable = true) $temporaryPath = $this->getTemporaryPathForFile($fileIdentifier); $this->s3Client->getObject([ 'Bucket' => $this->configuration['bucket'], - 'Key' => $fileIdentifier, + 'Key' => $this->addBaseFolder($fileIdentifier), 'SaveAs' => $temporaryPath, ]); if (!is_file($temporaryPath)) { @@ -599,7 +609,7 @@ public function getFileContents($fileIdentifier) { $result = $this->s3Client->getObject([ 'Bucket' => $this->configuration['bucket'], - 'Key' => $fileIdentifier + 'Key' => $this->addBaseFolder($fileIdentifier) ]); return (string)$result['Body']; } @@ -1083,6 +1093,19 @@ protected function initializeBaseUrl() return $this; } + /** + * Set the $baseFolder variable from configuration + */ + protected function initializeBaseFolder(): self + { + $baseFolder = $this->configuration['baseFolder'] ?? ''; + if ($baseFolder != '') { + $baseFolder = rtrim($baseFolder, '/') . '/'; + } + $this->baseFolder = $baseFolder; + return $this; + } + /** * initializeSettings * @@ -1183,6 +1206,30 @@ protected function testConnection() } } + /** + * Prefix the given file/path identifier with the base folder. + * Used by internal functions that directly speak with S3Client + */ + protected function addBaseFolder(string $identifier): string + { + if ($this->baseFolder) { + $identifier = str_replace('//', '/', $this->baseFolder . $identifier); + } + return $identifier; + } + + /** + * Remove base folder prefix from a given S3 path. + * Used when returning information from S3. + */ + protected function removeBaseFolder(string $identifier): string + { + if ($this->baseFolder) { + $identifier = substr($identifier, strlen($this->baseFolder)); + } + return $identifier; + } + /** * @return \TYPO3\CMS\Core\Messaging\FlashMessageQueue */ @@ -1241,7 +1288,7 @@ protected function getMetaInfo($identifier): ?array try { $metadata = $this->s3Client->headObject([ 'Bucket' => $this->configuration['bucket'], - 'Key' => $identifier + 'Key' => $this->addBaseFolder($identifier) ])->toArray(); $metaInfoDownloadAdapter = GeneralUtility::makeInstance(MetaInfoDownloadAdapter::class); $metaInfo = $metaInfoDownloadAdapter->getMetaInfoFromResponse($this, $identifier, $metadata); @@ -1257,6 +1304,7 @@ protected function getMetaInfo($identifier): ?array $this->metaInfoCache->remove($cacheIdentifier); return null; } + return $this->metaInfoCache->get($cacheIdentifier); } /** @@ -1330,7 +1378,7 @@ protected function getObjectPermissions($identifier) try { $response = $this->s3Client->getObjectAcl([ 'Bucket' => $this->configuration['bucket'], - 'Key' => $identifier + 'Key' => $this->addBaseFolder($identifier) ])->toArray(); // Until the SDK provides any useful information about folder permissions, we take full access for granted as long as one user with full access exists. @@ -1368,10 +1416,10 @@ protected function getObjectPermissions($identifier) */ protected function deleteObject(string $identifier): bool { - $this->s3Client->deleteObject(['Bucket' => $this->configuration['bucket'], 'Key' => $identifier]); + $this->s3Client->deleteObject(['Bucket' => $this->configuration['bucket'], 'Key' => $this->addBaseFolder($identifier)]); $this->flushMetaInfoCache($identifier); $this->resetRequestCache(); - return !$this->s3Client->doesObjectExist($this->configuration['bucket'], $identifier); + return !$this->s3Client->doesObjectExist($this->configuration['bucket'], $this->addBaseFolder($identifier)); } /** @@ -1399,7 +1447,7 @@ protected function createObject($identifier, $body = '', $overrideArgs = []) $this->normalizeIdentifier($identifier); $args = [ 'Bucket' => $this->configuration['bucket'], - 'Key' => $identifier, + 'Key' => $this->addBaseFolder($identifier), 'Body' => $body ]; $this->s3Client->putObject(array_merge_recursive($args, $overrideArgs)); @@ -1458,7 +1506,7 @@ protected function getStreamWrapperPath($file) throw new \RuntimeException('Type "' . gettype($file) . '" is not supported.', 1325191178); } $this->normalizeIdentifier($identifier); - return $basePath . $identifier; + return $basePath . $this->addBaseFolder($identifier); } /** @@ -1522,7 +1570,7 @@ protected function getListObjects($identifier, $overrideArgs = []) { $args = [ 'Bucket' => $this->configuration['bucket'] ?? '', - 'Prefix' => $identifier, + 'Prefix' => $this->addBaseFolder($identifier), ]; $result = $this->getCachedResponse('listObjectsV2', array_merge_recursive($args, $overrideArgs)); // Cache the given meta info @@ -1530,14 +1578,31 @@ protected function getListObjects($identifier, $overrideArgs = []) // with many files we come to the recursion which lessens the home of a cache hit, so we do not create the cache here if (isset($result['Contents']) && is_array($result['Contents'])) { - foreach ($result['Contents'] as $content) { - $fileIdentifier = $content['Key']; + $baseFolderSelfKey = null; + foreach ($result['Contents'] as $key => &$content) { + $content['Key'] = $this->removeBaseFolder($content['Key']); + if ($content['Key'] === '') { + $baseFolderSelfKey = $key; + continue; + } + $fileIdentifier = $identifier . $content['Key']; $this->normalizeIdentifier($fileIdentifier); $cacheIdentifier = md5($fileIdentifier); if (!$this->metaInfoCache->has($cacheIdentifier) || !$this->metaInfoCache->get($cacheIdentifier)) { $this->metaInfoCache->set($cacheIdentifier, $metaInfoDownloadAdapter->getMetaInfoFromResponse($this, $fileIdentifier, $content)); } } + unset($content); + if ($baseFolderSelfKey !== null) { + unset($result['Contents'][$baseFolderSelfKey]); + } + } + + if (isset($result['CommonPrefixes'])) { + foreach ($result['CommonPrefixes'] as &$prefix) { + $prefix['Prefix'] = $this->removeBaseFolder($prefix['Prefix']); + } + unset($prefix); } if (isset($overrideArgs['MaxKeys']) && $overrideArgs['MaxKeys'] <= 1000) { @@ -1591,8 +1656,8 @@ protected function copyObject($identifier, $targetIdentifier) { $this->s3Client->copyObject([ 'Bucket' => $this->configuration['bucket'], - 'CopySource' => $this->configuration['bucket'] . '/' . $identifier, - 'Key' => $targetIdentifier, + 'CopySource' => $this->configuration['bucket'] . '/' . $this->addBaseFolder($identifier), + 'Key' => $this->addBaseFolder($targetIdentifier), 'CacheControl' => $this->getCacheControl($targetIdentifier) ]); $this->flushMetaInfoCache($targetIdentifier); diff --git a/Configuration/FlexForm/AmazonS3DriverFlexForm.xml b/Configuration/FlexForm/AmazonS3DriverFlexForm.xml index 4c2f0211..0846f54f 100644 --- a/Configuration/FlexForm/AmazonS3DriverFlexForm.xml +++ b/Configuration/FlexForm/AmazonS3DriverFlexForm.xml @@ -187,6 +187,16 @@ + + + + LLL:EXT:aus_driver_amazon_s3/Resources/Private/Language/locallang_flexform.xlf:driverConfiguration.baseFolder-description + + input + 30 + + + diff --git a/Resources/Private/Language/locallang_flexform.xlf b/Resources/Private/Language/locallang_flexform.xlf index 9799c7a4..5c1dbd5b 100644 --- a/Resources/Private/Language/locallang_flexform.xlf +++ b/Resources/Private/Language/locallang_flexform.xlf @@ -33,6 +33,12 @@ Without protocol - "Protocol" setting is used here. + + Base folder + + + Root folder to use as base for all file operations. Other files and folders outside this base folder are not visible. + Cache header: max age (in seconds, optional)