diff --git a/lib/private/Files/ObjectStore/S3ConnectionTrait.php b/lib/private/Files/ObjectStore/S3ConnectionTrait.php index 7c7074c53c4c9..9405c2cf33492 100644 --- a/lib/private/Files/ObjectStore/S3ConnectionTrait.php +++ b/lib/private/Files/ObjectStore/S3ConnectionTrait.php @@ -33,303 +33,292 @@ use Aws\Credentials\EcsCredentialProvider; use Aws\Exception\CredentialsException; use Aws\S3\Exception\S3Exception; -use Aws\S3\ServerSideEncryptionConfigurationNotFoundError; use Aws\S3\S3Client; use GuzzleHttp\Promise; use GuzzleHttp\Promise\RejectedPromise; use OCP\ILogger; -trait S3ConnectionTrait -{ - /** @var array */ - protected $params; - - /** @var S3Client */ - protected $connection; - - /** @var string */ - protected $id; - - /** @var string */ - protected $bucket; - - /** @var int */ - protected $timeout; - - /** @var int */ - protected $uploadPartSize; - - /** @var string */ - protected $sseKmsKeyId; - - /** @var string */ - protected $sseKmsBucketKeyId; - - protected $test; - - protected function parseParams($params) - { - if (empty($params['bucket'])) { - throw new \Exception('Bucket has to be configured.'); - } - - $this->id = 'amazon::'.$params['bucket']; - - $this->test = isset($params['test']); - $this->bucket = $params['bucket']; - $this->timeout = !isset($params['timeout']) ? 15 : $params['timeout']; - $this->uploadPartSize = !isset($params['uploadPartSize']) ? 524288000 : $params['uploadPartSize']; - $params['region'] = empty($params['region']) ? 'eu-west-1' : $params['region']; - $params['hostname'] = empty($params['hostname']) ? 's3.'.$params['region'].'.amazonaws.com' : $params['hostname']; - if (!isset($params['port']) || $params['port'] === '') { - $params['port'] = (isset($params['use_ssl']) && $params['use_ssl'] === false) ? 80 : 443; - } - $params['autocreate'] = !isset($params['autocreate']) ? false : $params['autocreate']; - - // this avoid at least the hash lookups for each read/weite operation - if (isset($params['ssekmsbucketkeyid'])) { - $this->sseKmsBucketKeyId = $params['ssekmsbucketkeyid']; - } elseif (isset($params['ssekmskeyid'])) { - $this->sseKmsKeyId = $params['ssekmskeyid']; - } - - $this->params = $params; - } - - public function getBucket() - { - return $this->bucket; - } - - /** - * Add the SSE KMS parameterdepending on the - * KMS encryption strategy (bucket, individual or - * no encryption) for object creations. - * - * @return array with encryption parameters - */ - public function getSseKmsPutParameters(): array - { - if (isset($this->sseKmsBucketKeyId)) { - return [ - 'ServerSideEncryption' => 'aws:kms', - ]; - } elseif (isset($this->sseKmsKeyId)) { - return [ - 'ServerSideEncryption' => 'aws:kms', - 'SSEKMSKeyId' => $this->sseKmsKeyId, - ]; - } else { - return []; - } - } - - /** - * Add the SSE KMS parameter depending on the - * KMS encryption strategy (bucket, individual or - * no encryption) for object read. - * - * @return array with encryption parameters - */ - public function getSseKmsGetParameters(): array - { - if (isset($this->sseKmsBucketKeyId)) { - return [ - 'ServerSideEncryption' => 'aws:kms', - 'SSEKMSKeyId' => $this->sseKmsBucketKeyId, - ]; - } elseif (isset($this->sseKmsKeyId)) { - return [ - 'ServerSideEncryption' => 'aws:kms', - 'SSEKMSKeyId' => $this->sseKmsKeyId, - ]; - } else { - return []; - } - } - - - /** - * Create the required bucket - * - * @throws \Exception if bucket creation fails - */ - protected function createNewBucket() { - $logger = \OC::$server->getLogger(); - try { - $logger->info('Bucket "'.$this->bucket.'" does not exist - creating it.', ['app' => 'objectstore']); - if (!$this->connection::isBucketDnsCompatible($this->bucket)) { - throw new \Exception('The bucket will not be created because the name is not dns compatible, please correct it: '.$this->bucket); - } - $this->connection->createBucket(['Bucket' => $this->bucket]); - $this->testTimeout(); - } catch (S3Exception $e) { - $logger->logException($e, [ - 'message' => 'Invalid remote storage.', - 'level' => ILogger::DEBUG, - 'app' => 'objectstore', - ]); - throw new \Exception('Creation of bucket "'.$this->bucket.'" failed. '.$e->getMessage()); - } - } - - /** - * Check bucket key consistency or put bucket key if missing - * This operation only works for bucket owner or with - * s3:GetEncryptionConfiguration/s3:PutEncryptionConfiguration permission - * - * We recommend to use autocreate only on initial setup and - * use an S3:user only with object operation permission and no bucket operation permissions - * later with autocreate=false - * - * @throws \Exception if bucket key config is inconsistent or if putting the key fails - */ - protected function checkOrPutBucketKey() { - $logger = \OC::$server->getLogger(); - - try { - $encrypt_state = $this->connection->getBucketEncryption([ - 'Bucket' => $this->bucket, - ]); - return; - } - catch (S3Exception $e) { - try { - $logger->info('Bucket key for "'.$this->bucket.'" is not set - adding it.', ['app' => 'objectstore']); - $this->connection->putBucketEncryption([ - 'Bucket' => $this->bucket , - 'ServerSideEncryptionConfiguration' => [ - 'Rules' => [ - [ - 'ApplyServerSideEncryptionByDefault' => [ - 'KMSMasterKeyID' => $this->sseKmsBucketKeyId, - 'SSEAlgorithm' => 'aws:kms', - ], - 'BucketKeyEnabled' => true, - ], - ], - ], - ]); - $this->testTimeout(); - } catch (S3Exception $e) { - $logger->logException($e, [ - 'message' => 'Bucket key problem.', - 'level' => ILogger::DEBUG, - 'app' => 'objectstore', - ]); - throw new \Exception('Putting configured bucket key to "'.$this->bucket.'" failed. '.$e->getMessage()); - } - } - } - - - /** - * Returns the connection. - * - * @return S3Client connected client - * - * @throws \Exception if connection could not be made - */ - public function getConnection() - { - if (!is_null($this->connection)) { - return $this->connection; - } - - $scheme = (isset($this->params['use_ssl']) && $this->params['use_ssl'] === false) ? 'http' : 'https'; - $base_url = $scheme.'://'.$this->params['hostname'].':'.$this->params['port'].'/'; - - // Adding explicit credential provider to the beginning chain. - // Including environment variables and IAM instance profiles. - $provider = CredentialProvider::memoize( - CredentialProvider::chain( - $this->paramCredentialProvider(), - CredentialProvider::env(), - CredentialProvider::assumeRoleWithWebIdentityCredentialProvider(), - !empty(getenv(EcsCredentialProvider::ENV_URI)) - ? CredentialProvider::ecsCredentials() - : CredentialProvider::instanceProfile() - ) - ); - - $options = [ - 'version' => isset($this->params['version']) ? $this->params['version'] : 'latest', - 'credentials' => $provider, - 'endpoint' => $base_url, - 'region' => $this->params['region'], - 'use_path_style_endpoint' => isset($this->params['use_path_style']) ? $this->params['use_path_style'] : false, - 'signature_provider' => \Aws\or_chain([self::class, 'legacySignatureProvider'], ClientResolver::_default_signature_provider()), - 'csm' => false, - ]; - if (isset($this->params['proxy'])) { - $options['request.options'] = ['proxy' => $this->params['proxy']]; - } - if (isset($this->params['legacy_auth']) && $this->params['legacy_auth']) { - $options['signature_version'] = 'v2'; - } - $this->connection = new S3Client($options); - - if (!$this->connection::isBucketDnsCompatible($this->bucket)) { - $logger = \OC::$server->getLogger(); - $logger->debug('Bucket "'.$this->bucket.'" This bucket name is not dns compatible, it may contain invalid characters.', - ['app' => 'objectstore']); - } - - if ($this->params['autocreate'] && !$this->connection->doesBucketExist($this->bucket)) { - $this->createNewBucket(); - } - - if ($this->params['autocreate'] && isset($this->params['ssekmsbucketkeyid'])) { - $this->checkOrPutBucketKey(); - } - - // google cloud's s3 compatibility doesn't like the EncodingType parameter - if (strpos($base_url, 'storage.googleapis.com')) { - $this->connection->getHandlerList()->remove('s3.auto_encode'); - } - - return $this->connection; - } - - /** - * when running the tests wait to let the buckets catch up. - */ - private function testTimeout() - { - if ($this->test) { - sleep($this->timeout); - } - } - - public static function legacySignatureProvider($version, $service, $region) - { - switch ($version) { - case 'v2': - case 's3': - return new S3Signature(); - default: - return null; - } - } - - /** - * This function creates a credential provider based on user parameter file. - */ - protected function paramCredentialProvider(): callable - { - return function () { - $key = empty($this->params['key']) ? null : $this->params['key']; - $secret = empty($this->params['secret']) ? null : $this->params['secret']; - - if ($key && $secret) { - return Promise\promise_for( - new Credentials($key, $secret) - ); - } - - $msg = 'Could not find parameters set for credentials in config file.'; - - return new RejectedPromise(new CredentialsException($msg)); - }; - } +trait S3ConnectionTrait { + /** @var array */ + protected $params; + + /** @var S3Client */ + protected $connection; + + /** @var string */ + protected $id; + + /** @var string */ + protected $bucket; + + /** @var int */ + protected $timeout; + + /** @var int */ + protected $uploadPartSize; + + /** @var string */ + protected $sseKmsKeyId; + + /** @var string */ + protected $sseKmsBucketKeyId; + + protected $test; + + protected function parseParams($params) { + if (empty($params['bucket'])) { + throw new \Exception('Bucket has to be configured.'); + } + + $this->id = 'amazon::'.$params['bucket']; + + $this->test = isset($params['test']); + $this->bucket = $params['bucket']; + $this->timeout = !isset($params['timeout']) ? 15 : $params['timeout']; + $this->uploadPartSize = !isset($params['uploadPartSize']) ? 524288000 : $params['uploadPartSize']; + $params['region'] = empty($params['region']) ? 'eu-west-1' : $params['region']; + $params['hostname'] = empty($params['hostname']) ? 's3.'.$params['region'].'.amazonaws.com' : $params['hostname']; + if (!isset($params['port']) || $params['port'] === '') { + $params['port'] = (isset($params['use_ssl']) && $params['use_ssl'] === false) ? 80 : 443; + } + $params['autocreate'] = !isset($params['autocreate']) ? false : $params['autocreate']; + + // this avoid at least the hash lookups for each read/weite operation + if (isset($params['ssekmsbucketkeyid'])) { + $this->sseKmsBucketKeyId = $params['ssekmsbucketkeyid']; + } elseif (isset($params['ssekmskeyid'])) { + $this->sseKmsKeyId = $params['ssekmskeyid']; + } + + $this->params = $params; + } + + public function getBucket() { + return $this->bucket; + } + + /** + * Add the SSE KMS parameterdepending on the + * KMS encryption strategy (bucket, individual or + * no encryption) for object creations. + * + * @return array with encryption parameters + */ + public function getSseKmsPutParameters(): array { + if (isset($this->sseKmsBucketKeyId)) { + return [ + 'ServerSideEncryption' => 'aws:kms', + ]; + } elseif (isset($this->sseKmsKeyId)) { + return [ + 'ServerSideEncryption' => 'aws:kms', + 'SSEKMSKeyId' => $this->sseKmsKeyId, + ]; + } else { + return []; + } + } + + /** + * Add the SSE KMS parameter depending on the + * KMS encryption strategy (bucket, individual or + * no encryption) for object read. + * + * @return array with encryption parameters + */ + public function getSseKmsGetParameters(): array { + if (isset($this->sseKmsBucketKeyId)) { + return [ + 'ServerSideEncryption' => 'aws:kms', + 'SSEKMSKeyId' => $this->sseKmsBucketKeyId, + ]; + } elseif (isset($this->sseKmsKeyId)) { + return [ + 'ServerSideEncryption' => 'aws:kms', + 'SSEKMSKeyId' => $this->sseKmsKeyId, + ]; + } else { + return []; + } + } + + + /** + * Create the required bucket + * + * @throws \Exception if bucket creation fails + */ + protected function createNewBucket() { + $logger = \OC::$server->getLogger(); + try { + $logger->info('Bucket "'.$this->bucket.'" does not exist - creating it.', ['app' => 'objectstore']); + if (!$this->connection::isBucketDnsCompatible($this->bucket)) { + throw new \Exception('The bucket will not be created because the name is not dns compatible, please correct it: '.$this->bucket); + } + $this->connection->createBucket(['Bucket' => $this->bucket]); + $this->testTimeout(); + } catch (S3Exception $e) { + $logger->logException($e, [ + 'message' => 'Invalid remote storage.', + 'level' => ILogger::DEBUG, + 'app' => 'objectstore', + ]); + throw new \Exception('Creation of bucket "'.$this->bucket.'" failed. '.$e->getMessage()); + } + } + + /** + * Check bucket key consistency or put bucket key if missing + * This operation only works for bucket owner or with + * s3:GetEncryptionConfiguration/s3:PutEncryptionConfiguration permission + * + * We recommend to use autocreate only on initial setup and + * use an S3:user only with object operation permission and no bucket operation permissions + * later with autocreate=false + * + * @throws \Exception if bucket key config is inconsistent or if putting the key fails + */ + protected function checkOrPutBucketKey() { + $logger = \OC::$server->getLogger(); + + try { + $encrypt_state = $this->connection->getBucketEncryption([ + 'Bucket' => $this->bucket, + ]); + return; + } catch (S3Exception $e) { + try { + $logger->info('Bucket key for "'.$this->bucket.'" is not set - adding it.', ['app' => 'objectstore']); + $this->connection->putBucketEncryption([ + 'Bucket' => $this->bucket , + 'ServerSideEncryptionConfiguration' => [ + 'Rules' => [ + [ + 'ApplyServerSideEncryptionByDefault' => [ + 'KMSMasterKeyID' => $this->sseKmsBucketKeyId, + 'SSEAlgorithm' => 'aws:kms', + ], + 'BucketKeyEnabled' => true, + ], + ], + ], + ]); + $this->testTimeout(); + } catch (S3Exception $e) { + $logger->logException($e, [ + 'message' => 'Bucket key problem.', + 'level' => ILogger::DEBUG, + 'app' => 'objectstore', + ]); + throw new \Exception('Putting configured bucket key to "'.$this->bucket.'" failed. '.$e->getMessage()); + } + } + } + + + /** + * Returns the connection. + * + * @return S3Client connected client + * + * @throws \Exception if connection could not be made + */ + public function getConnection() { + if (!is_null($this->connection)) { + return $this->connection; + } + + $scheme = (isset($this->params['use_ssl']) && $this->params['use_ssl'] === false) ? 'http' : 'https'; + $base_url = $scheme.'://'.$this->params['hostname'].':'.$this->params['port'].'/'; + + // Adding explicit credential provider to the beginning chain. + // Including environment variables and IAM instance profiles. + $provider = CredentialProvider::memoize( + CredentialProvider::chain( + $this->paramCredentialProvider(), + CredentialProvider::env(), + CredentialProvider::assumeRoleWithWebIdentityCredentialProvider(), + !empty(getenv(EcsCredentialProvider::ENV_URI)) + ? CredentialProvider::ecsCredentials() + : CredentialProvider::instanceProfile() + ) + ); + + $options = [ + 'version' => isset($this->params['version']) ? $this->params['version'] : 'latest', + 'credentials' => $provider, + 'endpoint' => $base_url, + 'region' => $this->params['region'], + 'use_path_style_endpoint' => isset($this->params['use_path_style']) ? $this->params['use_path_style'] : false, + 'signature_provider' => \Aws\or_chain([self::class, 'legacySignatureProvider'], ClientResolver::_default_signature_provider()), + 'csm' => false, + ]; + if (isset($this->params['proxy'])) { + $options['request.options'] = ['proxy' => $this->params['proxy']]; + } + if (isset($this->params['legacy_auth']) && $this->params['legacy_auth']) { + $options['signature_version'] = 'v2'; + } + $this->connection = new S3Client($options); + + if (!$this->connection::isBucketDnsCompatible($this->bucket)) { + $logger = \OC::$server->getLogger(); + $logger->debug('Bucket "'.$this->bucket.'" This bucket name is not dns compatible, it may contain invalid characters.', + ['app' => 'objectstore']); + } + + if ($this->params['autocreate'] && !$this->connection->doesBucketExist($this->bucket)) { + $this->createNewBucket(); + } + + if ($this->params['autocreate'] && isset($this->params['ssekmsbucketkeyid'])) { + $this->checkOrPutBucketKey(); + } + + // google cloud's s3 compatibility doesn't like the EncodingType parameter + if (strpos($base_url, 'storage.googleapis.com')) { + $this->connection->getHandlerList()->remove('s3.auto_encode'); + } + + return $this->connection; + } + + /** + * when running the tests wait to let the buckets catch up. + */ + private function testTimeout() { + if ($this->test) { + sleep($this->timeout); + } + } + + public static function legacySignatureProvider($version, $service, $region) { + switch ($version) { + case 'v2': + case 's3': + return new S3Signature(); + default: + return null; + } + } + + /** + * This function creates a credential provider based on user parameter file. + */ + protected function paramCredentialProvider(): callable { + return function () { + $key = empty($this->params['key']) ? null : $this->params['key']; + $secret = empty($this->params['secret']) ? null : $this->params['secret']; + + if ($key && $secret) { + return Promise\promise_for( + new Credentials($key, $secret) + ); + } + + $msg = 'Could not find parameters set for credentials in config file.'; + + return new RejectedPromise(new CredentialsException($msg)); + }; + } } diff --git a/lib/private/Files/ObjectStore/S3ObjectTrait.php b/lib/private/Files/ObjectStore/S3ObjectTrait.php index 908a3458a1d5d..91c1ac34c3551 100644 --- a/lib/private/Files/ObjectStore/S3ObjectTrait.php +++ b/lib/private/Files/ObjectStore/S3ObjectTrait.php @@ -31,151 +31,129 @@ use Icewind\Streams\CallbackWrapper; use OC\Files\Stream\SeekableHttpStream; -trait S3ObjectTrait -{ - /** - * Returns the connection. - * - * @return S3Client connected client - * - * @throws \Exception if connection could not be made - */ - abstract protected function getConnection(); +trait S3ObjectTrait { + /** + * Returns the connection. + * + * @return S3Client connected client + * + * @throws \Exception if connection could not be made + */ + abstract protected function getConnection(); - /* compute configured encryption headers for put operations */ - abstract protected function getSseKmsPutParameters(); + /* compute configured encryption headers for put operations */ + abstract protected function getSseKmsPutParameters(); - /* compute configured encryption headers for get operations */ - abstract protected function getSseKmsGetParameters(); + /* compute configured encryption headers for get operations */ + abstract protected function getSseKmsGetParameters(); - /** - * @param string $urn the unified resource name used to identify the object - * - * @return resource stream with the read data - * - * @throws \Exception when something goes wrong, message will be logged - * - * @since 7.0.0 - */ - public function readObject($urn) - { - return SeekableHttpStream::open(function ($range) use ($urn) { - $s3params = [ - 'Bucket' => $this->bucket, - 'Key' => $urn, - 'Range' => 'bytes='.$range, - ] + $this->getSseKmsGetParameters(); - $command = $this->getConnection()->getCommand('GetObject', $s3params); - $request = \Aws\serialize($command); - $headers = []; - foreach ($request->getHeaders() as $key => $values) { - foreach ($values as $value) { - $headers[] = "$key: $value"; - } - } - $opts = [ - 'http' => [ - 'protocol_version' => 1.1, - 'header' => $headers, - ], - ]; + /** + * @param string $urn the unified resource name used to identify the object + * + * @return resource stream with the read data + * + * @throws \Exception when something goes wrong, message will be logged + * + * @since 7.0.0 + */ + public function readObject($urn) { + return SeekableHttpStream::open(function ($range) use ($urn) { + $s3params = [ + 'Bucket' => $this->bucket, + 'Key' => $urn, + 'Range' => 'bytes='.$range, + ] + $this->getSseKmsGetParameters(); + $command = $this->getConnection()->getCommand('GetObject', $s3params); + $request = \Aws\serialize($command); + $headers = []; + foreach ($request->getHeaders() as $key => $values) { + foreach ($values as $value) { + $headers[] = "$key: $value"; + } + } + $opts = [ + 'http' => [ + 'protocol_version' => 1.1, + 'header' => $headers, + ], + ]; - $context = stream_context_create($opts); + $context = stream_context_create($opts); + return fopen($request->getUri(), 'r', false, $context); + }); + } -<<<<<<< HEAD /** - * @param string $urn the unified resource name used to identify the object + * @param string $urn the unified resource name used to identify the object * @param resource $stream stream with the data to write - * @param string|null $mimetype the mimetype to set for the remove object @since 22.0.0 + * * @throws \Exception when something goes wrong, message will be logged + * * @since 7.0.0 */ - public function writeObject($urn, $stream, string $mimetype = null) { + public function writeObject($urn, $stream) { $count = 0; $countStream = CallbackWrapper::wrap($stream, function ($read) use (&$count) { $count += $read; }); -======= - return fopen($request->getUri(), 'r', false, $context); - }); - } ->>>>>>> Fix pull review findings - /** - * @param string $urn the unified resource name used to identify the object - * @param resource $stream stream with the data to write - * - * @throws \Exception when something goes wrong, message will be logged - * - * @since 7.0.0 - */ - public function writeObject($urn, $stream) - { - $count = 0; - $countStream = CallbackWrapper::wrap($stream, function ($read) use (&$count) { - $count += $read; - }); - - $s3params = [ - 'bucket' => $this->bucket, - 'key' => $urn, - 'part_size' => $this->uploadPartSize, - 'params' => $this->getSseKmsPutParameters(), - ]; - $uploader = new MultipartUploader($this->getConnection(), $countStream, $s3params); + $s3params = [ + 'bucket' => $this->bucket, + 'key' => $urn, + 'part_size' => $this->uploadPartSize, + 'params' => $this->getSseKmsPutParameters(), + ]; + $uploader = new MultipartUploader($this->getConnection(), $countStream, $s3params); - try { - $uploader->upload(); - } catch (S3MultipartUploadException $e) { - // This is an empty file so just touch it then - if ($count === 0 && feof($countStream)) { - $s3params = [ - 'params' => $this->getSseKmsPutParameters(), - ]; - $uploader = new ObjectUploader($this->getConnection(), $this->bucket, $urn, '', 'private', $s3params); - $uploader->upload(); - } else { - throw $e; - } - } finally { - // this handles [S3] fclose(): supplied resource is not a valid stream resource #23373 - // see https://stackoverflow.com/questions/11247507/fclose-18-is-not-a-valid-stream-resource/11247555 - // which also recommends the solution - if (is_resource($countStream)) { - fclose($countStream); - } - } - } + try { + $uploader->upload(); + } catch (S3MultipartUploadException $e) { + // This is an empty file so just touch it then + if ($count === 0 && feof($countStream)) { + $s3params = [ + 'params' => $this->getSseKmsPutParameters(), + ]; + $uploader = new ObjectUploader($this->getConnection(), $this->bucket, $urn, '', 'private', $s3params); + $uploader->upload(); + } else { + throw $e; + } + } finally { + // this handles [S3] fclose(): supplied resource is not a valid stream resource #23373 + // see https://stackoverflow.com/questions/11247507/fclose-18-is-not-a-valid-stream-resource/11247555 + // which also recommends the solution + if (is_resource($countStream)) { + fclose($countStream); + } + } + } - /** - * @param string $urn the unified resource name used to identify the object - * - * @return void - * - * @throws \Exception when something goes wrong, message will be logged - * - * @since 7.0.0 - */ - public function deleteObject($urn) - { - $this->getConnection()->deleteObject([ - 'Bucket' => $this->bucket, - 'Key' => $urn, - ]); - } + /** + * @param string $urn the unified resource name used to identify the object + * + * @return void + * + * @throws \Exception when something goes wrong, message will be logged + * + * @since 7.0.0 + */ + public function deleteObject($urn) { + $this->getConnection()->deleteObject([ + 'Bucket' => $this->bucket, + 'Key' => $urn, + ]); + } - public function objectExists($urn) - { - return $this->getConnection()->doesObjectExist($this->bucket, $urn); - } + public function objectExists($urn) { + return $this->getConnection()->doesObjectExist($this->bucket, $urn); + } - /** - * S3 copy command with SSE KMS key handling. - */ - public function copyObject($from, $to) - { - $this->getConnection()->copy($this->getBucket(), $from, $this->getBucket(), $to, 'private', [ - 'params' => $this->getSseKmsPutParameters(), - ]); - } + /** + * S3 copy command with SSE KMS key handling. + */ + public function copyObject($from, $to) { + $this->getConnection()->copy($this->getBucket(), $from, $this->getBucket(), $to, 'private', [ + 'params' => $this->getSseKmsPutParameters(), + ]); + } }