Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Add "base folder" configuration setting #116

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 81 additions & 16 deletions Classes/Driver/AmazonS3Driver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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()
Expand All @@ -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));
}

/**
Expand Down Expand Up @@ -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)
);
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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'];
}
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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);
Expand All @@ -1257,6 +1304,7 @@ protected function getMetaInfo($identifier): ?array
$this->metaInfoCache->remove($cacheIdentifier);
return null;
}
return $this->metaInfoCache->get($cacheIdentifier);
}

/**
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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));
}

/**
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -1522,22 +1570,39 @@ 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
$metaInfoDownloadAdapter = GeneralUtility::makeInstance(MetaInfoDownloadAdapter::class);

// 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) {
Expand Down Expand Up @@ -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);
Expand Down
10 changes: 10 additions & 0 deletions Configuration/FlexForm/AmazonS3DriverFlexForm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,16 @@
</config>
</TCEforms>
</publicBaseUrl>
<baseFolder>
<TCEforms>
<label>LLL:EXT:aus_driver_amazon_s3/Resources/Private/Language/locallang_flexform.xlf:driverConfiguration.baseFolder</label>
<description>LLL:EXT:aus_driver_amazon_s3/Resources/Private/Language/locallang_flexform.xlf:driverConfiguration.baseFolder-description</description>
<config>
<type>input</type>
<size>30</size>
</config>
</TCEforms>
</baseFolder>
<cacheHeaderDuration>
<TCEforms>
<label>LLL:EXT:aus_driver_amazon_s3/Resources/Private/Language/locallang_flexform.xlf:driverConfiguration.cacheHeaderDuration</label>
Expand Down
6 changes: 6 additions & 0 deletions Resources/Private/Language/locallang_flexform.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
<trans-unit id="driverConfiguration.publicBaseUrl-description">
<source>Without protocol - "Protocol" setting is used here.</source>
</trans-unit>
<trans-unit id="driverConfiguration.baseFolder">
<source>Base folder</source>
</trans-unit>
<trans-unit id="driverConfiguration.baseFolder-description">
<source>Root folder to use as base for all file operations. Other files and folders outside this base folder are not visible.</source>
</trans-unit>
<trans-unit id="driverConfiguration.cacheHeaderDuration">
<source>Cache header: max age (in seconds, optional)</source>
</trans-unit>
Expand Down
Loading