diff --git a/Classes/Command/S3CommandController.php b/Classes/Command/S3CommandController.php index 89d909d..9573314 100644 --- a/Classes/Command/S3CommandController.php +++ b/Classes/Command/S3CommandController.php @@ -9,8 +9,11 @@ use Aws\S3\BatchDelete; use Aws\S3\S3Client; +use Flownative\Aws\S3\S3Target; use Neos\Flow\Annotations as Flow; use Neos\Flow\Cli\CommandController; +use Neos\Flow\ResourceManagement\ResourceManager; +use Neos\Flow\ResourceManagement\Storage\StorageObject; /** * S3 command controller for the Flownative.Aws.S3 package @@ -25,6 +28,21 @@ class S3CommandController extends CommandController */ protected $s3DefaultProfile; + /** + * @Flow\Inject + * @var ResourceManager + */ + protected $resourceManager; + + /** + * @var S3Client + */ + private $s3Client; + + public function initializeObject() { + $this->s3Client = new S3Client($this->s3DefaultProfile); + } + /** * Checks the connection * @@ -41,12 +59,11 @@ class S3CommandController extends CommandController public function connectCommand(string $bucket = null, string $prefix = ''): void { try { - $s3Client = new S3Client($this->s3DefaultProfile); if ($bucket !== null) { - $s3Client->registerStreamWrapper(); + $this->s3Client->registerStreamWrapper(); $this->outputLine('Access list of objects in bucket "%s" with key prefix "%s" ...', [$bucket, $prefix]); - $s3Client->getPaginator('ListObjects', ['Bucket' => $bucket, 'Prefix' => $prefix]); + $this->s3Client->getPaginator('ListObjects', ['Bucket' => $bucket, 'Prefix' => $prefix]); $options = [ 'Bucket' => $bucket, @@ -56,16 +73,17 @@ public function connectCommand(string $bucket = null, string $prefix = ''): void 'Key' => $prefix . 'Flownative.Aws.S3.ConnectionTest.txt' ]; $this->outputLine('Writing test object into bucket (arn:aws:s3:::%s/%s) ...', [$bucket, $options['Key']]); - $s3Client->putObject($options); + $this->s3Client->putObject($options); $this->outputLine('Deleting test object from bucket ...'); $options = [ 'Bucket' => $bucket, 'Key' => $prefix . 'Flownative.Aws.S3.ConnectionTest.txt' ]; - $s3Client->deleteObject($options); + $this->s3Client->deleteObject($options); } else { - $s3Client->listBuckets(); + $this->outputLine('Listing buckets ...'); + $this->s3Client->listBuckets(); } } catch (\Exception $e) { $this->outputLine('<b>' . $e->getMessage() . '</b>'); @@ -74,6 +92,7 @@ public function connectCommand(string $bucket = null, string $prefix = ''): void } $this->quit(1); } + $this->outputLine(); $this->outputLine('OK'); } @@ -88,8 +107,7 @@ public function connectCommand(string $bucket = null, string $prefix = ''): void public function listBucketsCommand(): void { try { - $s3Client = new S3Client($this->s3DefaultProfile); - $result = $s3Client->listBuckets(); + $result = $this->s3Client->listBuckets(); } catch (\Exception $e) { $this->outputLine($e->getMessage()); $this->quit(1); @@ -121,8 +139,7 @@ public function listBucketsCommand(): void public function flushBucketCommand(string $bucket): void { try { - $s3Client = new S3Client($this->s3DefaultProfile); - $batchDelete = BatchDelete::fromListObjects($s3Client, ['Bucket' => $bucket]); + $batchDelete = BatchDelete::fromListObjects($this->s3Client, ['Bucket' => $bucket]); $promise = $batchDelete->promise(); } catch (\Exception $e) { $this->outputLine($e->getMessage()); @@ -157,8 +174,7 @@ public function uploadCommand(string $bucket, string $file, string $key = ''): v } try { - $s3Client = new S3Client($this->s3DefaultProfile); - $s3Client->putObject([ + $this->s3Client->putObject([ 'Key' => $key, 'Bucket' => $bucket, 'Body' => fopen('file://' . realpath($file), 'rb') @@ -170,4 +186,48 @@ public function uploadCommand(string $bucket, string $file, string $key = ''): v $this->outputLine('Successfully uploaded %s to %s::%s.', [$file, $bucket, $key]); } + + /** + * Republish a collection + * + * This command forces publishing resources of the given collection by copying resources from the respective storage + * to target bucket. + * + * @param string $collection Name of the collection to publish + */ + public function republishCommand(string $collection = 'persistent'): void + { + $collectionName = $collection; + $collection = $this->resourceManager->getCollection($collectionName); + if (!$collection) { + $this->outputLine('<error>The collection %s does not exist.</error>', [$collectionName]); + exit(1); + } + + $target = $collection->getTarget(); + if (!$target instanceof S3Target) { + $this->outputLine('<error>The target defined in collection %s is not an S3 target.</error>', [$collectionName]); + exit(1); + } + + $this->outputLine('Republishing collection ...'); + $this->output->progressStart(); + try { + foreach ($collection->getObjects() as $object) { + /** @var StorageObject $object */ + $resource = $this->resourceManager->getResourceBySha1($object->getSha1()); + if ($resource) { + $target->publishResource($resource, $collection); + } + $this->output->progressAdvance(); + } + } catch (\Exception $e) { + $this->outputLine('<error>Publishing failed</error>'); + $this->outputLine($e->getMessage()); + $this->outputLine(get_class($e)); + exit(2); + } + $this->output->progressFinish(); + $this->outputLine(); + } } diff --git a/Classes/S3Target.php b/Classes/S3Target.php index ee0a223..09e9ea4 100644 --- a/Classes/S3Target.php +++ b/Classes/S3Target.php @@ -9,6 +9,7 @@ use Aws\S3\Exception\S3Exception; use Aws\S3\S3Client; +use GuzzleHttp\Psr7\Uri; use Neos\Flow\Annotations as Flow; use Neos\Flow\Log\Utility\LogEnvironment; use Neos\Flow\ResourceManagement\CollectionInterface; @@ -17,6 +18,7 @@ use Neos\Flow\ResourceManagement\Publishing\MessageCollector; use Neos\Flow\ResourceManagement\ResourceManager; use Neos\Flow\ResourceManagement\ResourceMetaDataInterface; +use Neos\Flow\ResourceManagement\Storage\StorageInterface; use Neos\Flow\ResourceManagement\Storage\StorageObject; use Neos\Flow\ResourceManagement\Target\TargetInterface; use Psr\Log\LoggerInterface; @@ -62,6 +64,11 @@ class S3Target implements TargetInterface */ protected $keyPrefix; + /** + * @var string + */ + protected $persistentResourceUriPattern = ''; + /** * CORS (Cross-Origin Resource Sharing) allowed origins for published content * @@ -85,7 +92,7 @@ class S3Target implements TargetInterface /** * Internal cache for known storages, indexed by storage name * - * @var array<\Neos\Flow\ResourceManagement\Storage\StorageInterface> + * @var array<StorageInterface> */ protected $storages = []; @@ -123,6 +130,11 @@ class S3Target implements TargetInterface */ protected $existingObjectsInfo = []; + /** + * @var bool + */ + protected $bucketIsPublic; + /** * Constructor * @@ -152,6 +164,22 @@ public function __construct(string $name, array $options = []) break; case 'acl': $this->acl = (string)$value; + break; + case 'persistentResourceUris': + if (!is_array($value)) { + throw new Exception(sprintf('The option "%s" which was specified in the configuration of the "%s" resource S3Target is not a valid array. Please check your settings.', $key, $name), 1628259768); + } + foreach ($value as $uriOptionKey => $uriOptionValue) { + switch ($uriOptionKey) { + case 'pattern': + $this->persistentResourceUriPattern = (string)$uriOptionValue; + break; + default: + if ($uriOptionValue !== null) { + throw new Exception(sprintf('An unknown option "%s" was specified in the configuration of the "%s" resource S3Target. Please check your settings.', $uriOptionKey, $name), 1628259794); + } + } + } break; default: if ($value !== null) { @@ -216,6 +244,14 @@ public function getAcl() */ public function publishCollection(CollectionInterface $collection, callable $callback = null) { + $storage = $collection->getStorage(); + + if ($storage instanceof S3Storage && $storage->getBucketName() === $this->bucketName) { + // TODO do we need to update the content-type on the objects? + $this->systemLogger->debug(sprintf('Skipping resource publishing for bucket "%s", storage and target are the same.', $this->bucketName), LogEnvironment::fromMethodName(__METHOD__)); + return; + } + if (!isset($this->existingObjectsInfo)) { $this->existingObjectsInfo = []; $requestArguments = [ @@ -238,12 +274,9 @@ public function publishCollection(CollectionInterface $collection, callable $cal $potentiallyObsoleteObjects = array_fill_keys($this->existingObjectsInfo, true); - $storage = $collection->getStorage(); if ($storage instanceof S3Storage) { - $storageBucketName = $storage->getBucketName(); - if ($storageBucketName === $this->bucketName && $storage->getKeyPrefix() === $this->keyPrefix) { - throw new Exception(sprintf('Could not publish collection %s because the source and target S3 bucket is the same, with identical key prefixes. Either choose a different bucket or at least key prefix for the target.', $collection->getName()), 1428929137); - } + $this->publishCollectionFromS3Storage($collection, $storage, $potentiallyObsoleteObjects, $callback); + } else { foreach ($collection->getObjects($callback) as $object) { /** @var StorageObject $object */ $objectName = $this->keyPrefix . $this->getRelativePublicationPathAndFilename($object); @@ -297,6 +330,34 @@ public function publishCollection(CollectionInterface $collection, callable $cal } } + /** + * @param CollectionInterface $collection + * @param S3Storage $storage + * @param array $potentiallyObsoleteObjects + * @param callable|null $callback + */ + private function publishCollectionFromS3Storage(CollectionInterface $collection, S3Storage $storage, array &$potentiallyObsoleteObjects, callable $callback = null): void + { + foreach ($collection->getObjects($callback) as $object) { + /** @var StorageObject $object */ + $objectName = $this->keyPrefix . $this->getRelativePublicationPathAndFilename($object); + if (array_key_exists($objectName, $potentiallyObsoleteObjects)) { + $this->systemLogger->debug(sprintf('The resource object "%s" (SHA1: %s) has already been published to bucket "%s", no need to re-publish', $objectName, $object->getSha1() ?: 'unknown', $this->bucketName)); + $potentiallyObsoleteObjects[$objectName] = false; + } else { + $this->copyObject( + function (StorageObject $object) use ($storage): string { + return $this->bucketName . '/' . $storage->getKeyPrefix() . $object->getSha1(); + }, + function (StorageObject $object) use ($storage): string { + return $storage->getKeyPrefix() . $this->getRelativePublicationPathAndFilename($object); + }, + $object + ); + } + } + } + /** * Returns the web accessible URI pointing to the given static resource * @@ -326,30 +387,29 @@ public function publishResource(PersistentResource $resource, CollectionInterfac { $storage = $collection->getStorage(); if ($storage instanceof S3Storage) { - if ($storage->getBucketName() === $this->bucketName && $storage->getKeyPrefix() === $this->keyPrefix) { - throw new Exception(sprintf('Could not publish resource with SHA1 hash %s of collection %s because the source and target S3 bucket is the same, with identical key prefixes. Either choose a different bucket or at least key prefix for the target.', $resource->getSha1(), $collection->getName()), 1428929563); + if ($storage->getBucketName() === $this->bucketName) { + // to update the Content-Type the object must be copied to itself… + $this->copyObject( + function (PersistentResource $resource) use ($storage): string { + return $this->bucketName . '/' . $storage->getKeyPrefix() . $resource->getSha1(); + }, + function (PersistentResource $resource) use ($storage): string { + return $storage->getKeyPrefix() . $resource->getSha1(); + }, + $resource + ); + return; } - $sourceObjectArn = $storage->getBucketName() . '/' . $storage->getKeyPrefix() . $resource->getSha1(); - $objectName = $this->keyPrefix . $this->getRelativePublicationPathAndFilename($resource); - try { - $options = [ - 'Bucket' => $this->bucketName, - 'CopySource' => urlencode($sourceObjectArn), - 'ContentType' => $resource->getMediaType(), - 'MetadataDirective' => 'REPLACE', - 'Key' => $objectName - ]; - if ($this->getAcl()) { - $options['ACL'] = $this->getAcl(); - } - $this->s3Client->copyObject($options); - $this->systemLogger->debug(sprintf('Successfully published resource as object "%s" (SHA1: %s) by copying from bucket "%s" to bucket "%s"', $objectName, $resource->getSha1() ?: 'unknown', $storage->getBucketName(), $this->bucketName)); - } catch (S3Exception $e) { - $message = sprintf('Could not publish resource with SHA1 hash %s of collection %s (source object: %s) through "CopyObject" because the S3 client reported an error: %s', $resource->getSha1(), $collection->getName(), $sourceObjectArn, $e->getMessage()); - $this->systemLogger->critical($e, LogEnvironment::fromMethodName(__METHOD__)); - $this->messageCollector->append($message); - } + $this->copyObject( + function (PersistentResource $resource) use ($storage): string { + return urlencode($storage->getBucketName() . '/' . $storage->getKeyPrefix() . $resource->getSha1()); + }, + function (PersistentResource $resource): string { + return $this->keyPrefix . $this->getRelativePublicationPathAndFilename($resource); + }, + $resource + ); } else { $sourceStream = $resource->getStream(); if ($sourceStream === false) { @@ -361,6 +421,31 @@ public function publishResource(PersistentResource $resource, CollectionInterfac } } + private function copyObject(\Closure $sourceBuilder, \Closure $targetBuilder, ResourceMetaDataInterface $resource): void + { + $source = $sourceBuilder($resource); + $target = $targetBuilder($resource); + + $options = [ + 'Bucket' => $this->bucketName, + 'CopySource' => $source, + 'ContentType' => $resource->getMediaType(), + 'MetadataDirective' => 'REPLACE', + 'Key' => $target + ]; + if ($this->getAcl()) { + $options['ACL'] = $this->getAcl(); + } + try { + $this->s3Client->copyObject($options); + $this->systemLogger->debug(sprintf('Successfully published resource as object "%s" (SHA1: %s) by copying from "%s" to bucket "%s"', $target, $resource->getSha1() ?: 'unknown', $source, $this->bucketName)); + } catch (S3Exception $e) { + $this->systemLogger->critical($e, LogEnvironment::fromMethodName(__METHOD__)); + $message = sprintf('Could not publish resource with SHA1 hash %s of collection %s (source object: %s) through "CopyObject" because the S3 client reported an error: %s', $resource->getSha1(), $collection->getName(), $source, $e->getMessage()); + $this->messageCollector->append($message); + } + } + /** * Unpublishes the given persistent resource * @@ -374,6 +459,13 @@ public function unpublishResource(PersistentResource $resource) return; } + $storage = $this->resourceManager->getCollection($resource->getCollectionName())->getStorage(); + if ($storage instanceof S3Storage && $storage->getBucketName() === $this->bucketName) { + // Unpublish for same-bucket setups is a NOOP, because the storage object will already be deleted. + $this->systemLogger->debug(sprintf('Skipping resource unpublishing %s from bucket "%s", because storage and target are the same.', $resource->getSha1() ?: 'unknown', $this->bucketName)); + return; + } + try { $objectName = $this->keyPrefix . $this->getRelativePublicationPathAndFilename($resource); $this->s3Client->deleteObject([ @@ -393,11 +485,31 @@ public function unpublishResource(PersistentResource $resource) */ public function getPublicPersistentResourceUri(PersistentResource $resource): string { - if ($this->baseUri !== '') { + + if (empty($this->persistentResourceUriPattern)) { + if (empty($this->baseUri)) { + return $this->s3Client->getObjectUrl($this->bucketName, $this->keyPrefix . $this->getRelativePublicationPathAndFilename($resource)); + } + return $this->baseUri . $this->encodeRelativePathAndFilenameForUri($this->getRelativePublicationPathAndFilename($resource)); } - return $this->s3Client->getObjectUrl($this->bucketName, $this->keyPrefix . $this->getRelativePublicationPathAndFilename($resource)); + $variables = [ + '{baseUri}' => $this->baseUri, + '{bucketName}' => $this->bucketName, + '{keyPrefix}' => $this->keyPrefix, + '{sha1}' => $resource->getSha1(), + '{filename}' => $resource->getFilename(), + '{fileExtension}' => $resource->getFileExtension() + ]; + + $customUri = $this->persistentResourceUriPattern; + foreach ($variables as $placeholder => $replacement) { + $customUri = str_replace($placeholder, $replacement, $customUri); + } + + // Let Uri implementation take care of encoding the Uri + return (string)new Uri($customUri); } /** diff --git a/README.md b/README.md index c1d905e..d58bb31 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,48 @@ Flownative: region: 'eu-central-1' ``` +Right now, you can only define one connection profile, namely the "default" profile. Additional profiles may be supported +in future versions. + +### Set up bucket(s) for your data + +You can either create separate buckets for storage and target respectively or use the same bucket as storage +and target. + +How you name those buckets is up to you, the names will be used in the configuration later on. + +#### One Bucket + +In a one-bucket setup, the same bucket will be used as storage and target. All resources are publicly accessible, +so Flow can render a URL pointing to a resource right after it was uploaded. + +**Note:** The bucket must not block public access to it's contents if you want to serve directly from the bucket. + +This setup is fast and saves storage space, because resources do not have to be copied and are only stored once. +On the downside, the URLs are kind of ugly, because they only consist of a domain, path and the resource's SHA1: + +``` +https://s3.eu-central-1.amazonaws.com/storage.neos.example.com/sites/wwwexamplecom/00889c4636cd77876e154796d469955e567ce23c +``` + +To have meaningful filenames you need to install a reverse proxy with path rewriting rules in order to simulate these +filenames or use a CDN (e.g. CloudFront.). + +#### Two Buckets + +In a two-bucket setup, resources will be duplicated: the original is stored in the "storage" bucket and then +copied to the "target" bucket. Each time a new resource is created or imported, it will be stored in the +storage bucket and then automatically published (i.e. copied) into the target bucket. + +**Note:** The target bucket must not block public access to it's contents if you want to serve directly from the +bucket. + +You may choose this setup in order to have human- and SEO-friendly URLs pointing to your resources, because +objects copied into the target bucket can have a more telling name which includes the original filename of +the resource (see for the `publicPersistentResourceUris` options further below). + +### Test your settings + You can test your settings by executing the `connect` command. If you restricted access to a particular sub path of a bucket, you must specify the bucket and key prefix: @@ -57,12 +99,9 @@ a bucket, you must specify the bucket and key prefix: ``` Note that it does make a difference if you specify the prefix with a leading slash "/" or without, because the corresponding -policy must match the pattern correctly, as you can see in the next section. +policy must match the pattern correctly, as you can see later. -Right now, you can only define one connection profile, namely the "default" profile. Additional profiles may be supported -in future versions. - -## IAM Setup +### IAM Setup It is best practice to create a user through AWS' Identity and Access Management which is exclusively used for your Flow or Neos instance to communicate with S3. This user needs minimal access rights, which can be defined either by @@ -131,7 +170,7 @@ for a bucket operation](http://docs.aws.amazon.com/AmazonS3/latest/dev/access-co ## Publish Assets to S3 / Cloudfront -Once the connector package is in place, you add a new publishing target which uses that connect and assign this target +Once the connector package is in place, you add a new publishing target which uses that connector and assign this target to your collection. ```yaml @@ -160,6 +199,46 @@ Since the new publishing target will be empty initially, you need to publish you This command will upload your files to the target and use the calculated remote URL for all your assets from now on. +## Customizing the Public URLs + +The S3 target supports a way to customize the URLs which are presented to the user. Even +though the paths and filenames used for objects in the buckets is rather fixed (see above for the `baseUri` and +`keyPrefix` options), you may want to use a reverse proxy or content delivery network to deliver resources +stored in your target bucket. In that case, you can tell the Target to render URLs according to your own rules. +It is your responsibility then to make sure that these URLs actually work. + +The behaviour depends on the setup being used: + +- no pattern and no baseUri set: the URL the S3 client returns for the resource +- no pattern set: the baseUri, followed by the relative publication path of the + resource (if any) or the SHA1, followed by the filename + +Let's assume that we have set up a webserver acting as a reverse proxy. Requests to `assets.flownative.com` are +re-written so that using a URI like `https://assets.flownative.com/a817…cb1/logo.svg` will actually deliver +a file stored in the Storage bucket using the given SHA1. + +You can tell the Target to render URIs like these by defining a pattern with placeholders: + +```yaml + targets: + s3PersistentResourcesTarget: + target: 'Flownative\Aws\S3\S3Target' + targetOptions: + bucket: 'flownativecom.flownative.cloud' + baseUri: 'https://assets.flownative.com/' + persistentResourceUris: + pattern: '{baseUri}{sha1}/{filename}' +``` + +The possible placeholders are: + +- `{baseUri}` The base URI as defined in the target options +- `{bucketName}` The target's bucket name +- `{keyPrefix}` The target's configured key prefix +- `{sha1}` The resource's SHA1 +- `{filename}` The resource's full filename, for example "logo.svg" +- `{fileExtension}` The resource's file extension, for example "svg" + ## Switching the Storage of a Collection If you want to migrate from your default local filesystem storage to a remote storage, you need to copy all your existing @@ -190,15 +269,14 @@ assets by the remote storage system, you also add a target that contains your pu Some notes regarding the configuration: -You must create separate buckets for storage and target respectively, because the storage will remain private and the -target will potentially be published. Even if it might work using one bucket for both, this is a non-supported setup. - -The `keyPrefix` option allows you to share one bucket accross multiple websites or applications. All S3 objects keys -will be prefiexd by the given string. +The `keyPrefix` option allows you to share one bucket across multiple websites or applications. All S3 objects keys +will be prefixed by the given string. The `baseUri` option defines the root of the publicly accessible address pointing to your published resources. In the example above, baseUri points to a Cloudfront subdomain which needs to be set up separately. It is rarely a good idea to -the public URI of S3 objects directly (like, for example "https://s3.eu-central-1.amazonaws.com/target.neos.example.com/sites/wwwexamplecom/00889c4636cd77876e154796d469955e567ce23c/NeosCMS-2507x3347.jpg") because S3 is usually too slow for being used as a server for common assets on your website. It's good for downloads, but not for your CSS files or photos. +the public URI of S3 objects directly (like, for example "https://s3.eu-central-1.amazonaws.com/target.neos.example.com/sites/wwwexamplecom/00889c4636cd77876e154796d469955e567ce23c/NeosCMS-2507x3347.jpg") because S3 is usually too +slow for being used as a server for common assets on your website. It's good for downloads, but not for your CSS files +or photos. In order to copy the resources to the new storage we need a temporary collection that uses the storage and the new publication target. @@ -214,7 +292,7 @@ publication target. target: 's3PersistentResourcesTarget' ``` -Now you can use the ``resource:copy`` command (available in Flow 3.1 or Neos 2.1 and higher): +Now you can use the ``resource:copy`` command: ```bash @@ -247,6 +325,43 @@ Clear caches and you're done. ``` +## Preventing unpublishing of resources in the target + +There are certain situations (e.g. when having a two-stack CMS setup), where one needs to prevent unpublishing of images +or other resources, for some time. + +Thus, the S3 Target option `unpublishResources` can be set to `false`, to prevent removing data from the S3 Target: + +```yaml +Neos: + Flow: + resource: + targets: + s3PersistentResourcesTarget: + target: 'Flownative\Aws\S3\S3Target' + targetOptions: + unpublishResources: false + # ... other options here ... +``` + +## Disable public-read ACL + +The ACL for a target defaults to the setting "Flownative.Aws.S3.profiles.default.acl" but can be overwritten via targetOption "acl". + +So in case you want a different ACL than "public-read", e.g. when using CloudFront with conflicting restrictive policies. +You can either just set the above configuration setting or adjust your specific target configuration: + +```yaml +Neos: + Flow: + resource: + targets: + s3PersistentResourcesTarget: + target: 'Flownative\Aws\S3\S3Target' + targetOptions: + acl: '' +``` + ## Full Example Configuration for S3 ```yaml @@ -295,8 +410,25 @@ Flownative: region: 'eu-central-1' ``` +## Path-style endpoints + +When using a custom endpoint for a non-AWS, S3-compatible storage, the use of this option may be needed. + +```yaml +Flownative: + Aws: + S3: + profiles: + default: + endpoint: 'https://abc.objectstorage.provider.tld/my-bucket-name' + # Prevents the AWS client to prepend the bucket name to the hostname + use_path_style_endpoint: true +``` + ## Using Google Cloud Storage +Note: It might be simple to use our ![Flow GCS connector](https://packagist.org/packages/flownative/google-cloudstorage) instead. + Google Cloud Storage (GCS) is an offering by Google which is very similar to AWS S3. In fact, GCS supports an S3-compatible endpoint which allows you to use Google's storage as a replacement for Amazon's S3. However, note that if you access GCS through the S3 compatible service endpoint, you won't be able to use the full feature set of Google Cloud @@ -328,55 +460,3 @@ Flownative: secret: 'abcdefgHIJKLMNOP1234567890QRSTUVWXYZabcd' endpoint: 'https://storage.googleapis.com/mybucket.flownative.net' ``` - -## Path-style endpoints - -When using a custom endpoint for a non-AWS, S3-compatible storage, the use of this option may be needed. - -```yaml -Flownative: - Aws: - S3: - profiles: - default: - endpoint: 'https://abc.objectstorage.provider.tld/my-bucket-name' - # Prevents the AWS client to prepend the bucket name to the hostname - use_path_style_endpoint: true -``` - -## Preventing unpublishing of resources in the target - -There are certain situations (e.g. when having a two-stack CMS setup), where one needs to prevent unpublishing of images -or other resources, for some time. - -Thus, the S3 Target option `unpublishResources` can be set to `false`, to prevent removing data from the S3 Target: - -```yaml -Neos: - Flow: - resource: - targets: - s3PersistentResourcesTarget: - target: 'Flownative\Aws\S3\S3Target' - targetOptions: - unpublishResources: false - # ... other options here ... -``` - -## Disable public-read ACL - -The ACL for a target defaults to the setting "Flownative.Aws.S3.profiles.default.acl" but can be overwritten via targetOption "acl". - -So in case you want a different ACL than "public-read", e.g. when using CloudFront with conflicting restrictive policies. -You can either just set the above configuration setting or adjust your specific target configuration: - -```yaml -Neos: - Flow: - resource: - targets: - s3PersistentResourcesTarget: - target: 'Flownative\Aws\S3\S3Target' - targetOptions: - acl: '' -```