Skip to content

Commit

Permalink
[FEATURE] Add "base folder" configuration setting
Browse files Browse the repository at this point in the history
The base folder allows us to use the same bucket for multiple projects
by forcing every project/TYPO3 instance into an own prefix:

```
mybucket/
+ project1/
| + file23.jpg
| + subfolder/
| | + subfile42.png
| + _processed_/
|   +  ...
+ project2/
  + file2.jpg
  + _processed_/
    +  ...
```

The prefix is not visible in the TYPO3 UI (file list, file information)
and gets added transparently to S3 requests, and is removed
from S3 responses as well.

My initial version used a simpler approach by just overriding
getRootLevelFolder() and getParentFolderIdentifierOfIdentifier(),
but this led to inconsistencies in the UI, and the processing folder
could not be moved into the base folder because of inconsistencies
inside the TYPO3 API.
  • Loading branch information
cweiske committed Dec 1, 2023
1 parent a374716 commit 12fc796
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 15 deletions.
93 changes: 78 additions & 15 deletions Classes/Driver/AmazonS3Driver.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,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 @@ -217,6 +225,7 @@ public function processConfiguration()
public function initialize()
{
$this->initializeBaseUrl()
->initializeBaseFolder()
->initializeSettings()
->initializeClient();
$this->resetRequestCache();
Expand All @@ -237,7 +246,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 @@ -393,14 +402,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 @@ -523,7 +532,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 @@ -598,7 +607,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 @@ -1081,6 +1090,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 @@ -1181,6 +1203,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 @@ -1234,7 +1280,7 @@ protected function getMetaInfo($identifier)
try {
$metadata = $this->s3Client->headObject([
'Bucket' => $this->configuration['bucket'],
'Key' => $identifier
'Key' => $this->addBaseFolder($identifier)
])->toArray();
$metaInfoDownloadAdapter = GeneralUtility::makeInstance(MetaInfoDownloadAdapter::class);
$this->metaInfoCache[$identifier] = $metaInfoDownloadAdapter->getMetaInfoFromResponse($this, $identifier, $metadata);
Expand Down Expand Up @@ -1313,7 +1359,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 @@ -1351,10 +1397,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 @@ -1382,7 +1428,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 @@ -1441,7 +1487,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 @@ -1505,20 +1551,37 @@ 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);
if (isset($result['Contents']) && is_array($result['Contents'])) {
foreach ($result['Contents'] as $content) {
$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);
if (!isset($this->metaInfoCache[$fileIdentifier])) {
$this->metaInfoCache[$fileIdentifier] = $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 @@ -1572,8 +1635,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

0 comments on commit 12fc796

Please sign in to comment.