diff --git a/manifest.json b/manifest.json index eadfb4d05..bb95605ee 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "variables": { - "${LATEST}": "3.330.0" + "${LATEST}": "3.330.1" }, "endpoints": "https://raw.githubusercontent.com/aws/aws-sdk-php/${LATEST}/src/data/endpoints.json", "services": { diff --git a/src/Service/S3/CHANGELOG.md b/src/Service/S3/CHANGELOG.md index 840efb24b..a91945fd5 100644 --- a/src/Service/S3/CHANGELOG.md +++ b/src/Service/S3/CHANGELOG.md @@ -2,6 +2,10 @@ ## NOT RELEASED +### Added + +- AWS api-change: Amazon Simple Storage Service / Features: Add support for ETag based conditional writes in PutObject and CompleteMultiPartUpload APIs to prevent unintended object modifications. + ## 2.5.0 ### Added diff --git a/src/Service/S3/composer.json b/src/Service/S3/composer.json index f08e84c7a..2da72119e 100644 --- a/src/Service/S3/composer.json +++ b/src/Service/S3/composer.json @@ -30,7 +30,7 @@ }, "extra": { "branch-alias": { - "dev-master": "2.5-dev" + "dev-master": "2.6-dev" } } } diff --git a/src/Service/S3/src/Input/CompleteMultipartUploadRequest.php b/src/Service/S3/src/Input/CompleteMultipartUploadRequest.php index d28ff2c56..069464355 100644 --- a/src/Service/S3/src/Input/CompleteMultipartUploadRequest.php +++ b/src/Service/S3/src/Input/CompleteMultipartUploadRequest.php @@ -127,6 +127,26 @@ final class CompleteMultipartUploadRequest extends Input */ private $expectedBucketOwner; + /** + * Uploads the object only if the ETag (entity tag) value provided during the WRITE operation matches the ETag of the + * object in S3. If the ETag values do not match, the operation returns a `412 Precondition Failed` error. + * + * If a conflicting operation occurs during the upload S3 returns a `409 ConditionalRequestConflict` response. On a 409 + * failure you should fetch the object's ETag, re-initiate the multipart upload with `CreateMultipartUpload`, and + * re-upload each part. + * + * Expects the ETag value as a string. + * + * For more information about conditional requests, see RFC 7232 [^1], or Conditional requests [^2] in the *Amazon S3 + * User Guide*. + * + * [^1]: https://tools.ietf.org/html/rfc7232 + * [^2]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-requests.html + * + * @var string|null + */ + private $ifMatch; + /** * Uploads the object only if the object key name does not already exist in the bucket specified. Otherwise, Amazon S3 * returns a `412 Precondition Failed` error. @@ -197,6 +217,7 @@ final class CompleteMultipartUploadRequest extends Input * ChecksumSHA256?: null|string, * RequestPayer?: null|RequestPayer::*, * ExpectedBucketOwner?: null|string, + * IfMatch?: null|string, * IfNoneMatch?: null|string, * SSECustomerAlgorithm?: null|string, * SSECustomerKey?: null|string, @@ -216,6 +237,7 @@ public function __construct(array $input = []) $this->checksumSha256 = $input['ChecksumSHA256'] ?? null; $this->requestPayer = $input['RequestPayer'] ?? null; $this->expectedBucketOwner = $input['ExpectedBucketOwner'] ?? null; + $this->ifMatch = $input['IfMatch'] ?? null; $this->ifNoneMatch = $input['IfNoneMatch'] ?? null; $this->sseCustomerAlgorithm = $input['SSECustomerAlgorithm'] ?? null; $this->sseCustomerKey = $input['SSECustomerKey'] ?? null; @@ -235,6 +257,7 @@ public function __construct(array $input = []) * ChecksumSHA256?: null|string, * RequestPayer?: null|RequestPayer::*, * ExpectedBucketOwner?: null|string, + * IfMatch?: null|string, * IfNoneMatch?: null|string, * SSECustomerAlgorithm?: null|string, * SSECustomerKey?: null|string, @@ -277,6 +300,11 @@ public function getExpectedBucketOwner(): ?string return $this->expectedBucketOwner; } + public function getIfMatch(): ?string + { + return $this->ifMatch; + } + public function getIfNoneMatch(): ?string { return $this->ifNoneMatch; @@ -348,6 +376,9 @@ public function request(): Request if (null !== $this->expectedBucketOwner) { $headers['x-amz-expected-bucket-owner'] = $this->expectedBucketOwner; } + if (null !== $this->ifMatch) { + $headers['If-Match'] = $this->ifMatch; + } if (null !== $this->ifNoneMatch) { $headers['If-None-Match'] = $this->ifNoneMatch; } @@ -433,6 +464,13 @@ public function setExpectedBucketOwner(?string $value): self return $this; } + public function setIfMatch(?string $value): self + { + $this->ifMatch = $value; + + return $this; + } + public function setIfNoneMatch(?string $value): self { $this->ifNoneMatch = $value; diff --git a/src/Service/S3/src/Input/PutObjectRequest.php b/src/Service/S3/src/Input/PutObjectRequest.php index 66776918a..e9195c640 100644 --- a/src/Service/S3/src/Input/PutObjectRequest.php +++ b/src/Service/S3/src/Input/PutObjectRequest.php @@ -251,6 +251,25 @@ final class PutObjectRequest extends Input */ private $expires; + /** + * Uploads the object only if the ETag (entity tag) value provided during the WRITE operation matches the ETag of the + * object in S3. If the ETag values do not match, the operation returns a `412 Precondition Failed` error. + * + * If a conflicting operation occurs during the upload S3 returns a `409 ConditionalRequestConflict` response. On a 409 + * failure you should fetch the object's ETag and retry the upload. + * + * Expects the ETag value as a string. + * + * For more information about conditional requests, see RFC 7232 [^1], or Conditional requests [^2] in the *Amazon S3 + * User Guide*. + * + * [^1]: https://tools.ietf.org/html/rfc7232 + * [^2]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-requests.html + * + * @var string|null + */ + private $ifMatch; + /** * Uploads the object only if the object key name does not already exist in the bucket specified. Otherwise, Amazon S3 * returns a `412 Precondition Failed` error. @@ -597,6 +616,7 @@ final class PutObjectRequest extends Input * ChecksumSHA1?: null|string, * ChecksumSHA256?: null|string, * Expires?: null|\DateTimeImmutable|string, + * IfMatch?: null|string, * IfNoneMatch?: null|string, * GrantFullControl?: null|string, * GrantRead?: null|string, @@ -641,6 +661,7 @@ public function __construct(array $input = []) $this->checksumSha1 = $input['ChecksumSHA1'] ?? null; $this->checksumSha256 = $input['ChecksumSHA256'] ?? null; $this->expires = !isset($input['Expires']) ? null : ($input['Expires'] instanceof \DateTimeImmutable ? $input['Expires'] : new \DateTimeImmutable($input['Expires'])); + $this->ifMatch = $input['IfMatch'] ?? null; $this->ifNoneMatch = $input['IfNoneMatch'] ?? null; $this->grantFullControl = $input['GrantFullControl'] ?? null; $this->grantRead = $input['GrantRead'] ?? null; @@ -685,6 +706,7 @@ public function __construct(array $input = []) * ChecksumSHA1?: null|string, * ChecksumSHA256?: null|string, * Expires?: null|\DateTimeImmutable|string, + * IfMatch?: null|string, * IfNoneMatch?: null|string, * GrantFullControl?: null|string, * GrantRead?: null|string, @@ -835,6 +857,11 @@ public function getGrantWriteAcp(): ?string return $this->grantWriteAcp; } + public function getIfMatch(): ?string + { + return $this->ifMatch; + } + public function getIfNoneMatch(): ?string { return $this->ifNoneMatch; @@ -993,6 +1020,9 @@ public function request(): Request if (null !== $this->expires) { $headers['Expires'] = $this->expires->setTimezone(new \DateTimeZone('GMT'))->format(\DateTimeInterface::RFC7231); } + if (null !== $this->ifMatch) { + $headers['If-Match'] = $this->ifMatch; + } if (null !== $this->ifNoneMatch) { $headers['If-None-Match'] = $this->ifNoneMatch; } @@ -1262,6 +1292,13 @@ public function setGrantWriteAcp(?string $value): self return $this; } + public function setIfMatch(?string $value): self + { + $this->ifMatch = $value; + + return $this; + } + public function setIfNoneMatch(?string $value): self { $this->ifNoneMatch = $value; diff --git a/src/Service/S3/src/S3Client.php b/src/Service/S3/src/S3Client.php index d65fda8dc..ee7307e01 100644 --- a/src/Service/S3/src/S3Client.php +++ b/src/Service/S3/src/S3Client.php @@ -347,6 +347,7 @@ public function bucketNotExists($input): BucketNotExistsWaiter * ChecksumSHA256?: null|string, * RequestPayer?: null|RequestPayer::*, * ExpectedBucketOwner?: null|string, + * IfMatch?: null|string, * IfNoneMatch?: null|string, * SSECustomerAlgorithm?: null|string, * SSECustomerKey?: null|string, @@ -2467,6 +2468,7 @@ public function putBucketTagging($input): Result * ChecksumSHA1?: null|string, * ChecksumSHA256?: null|string, * Expires?: null|\DateTimeImmutable|string, + * IfMatch?: null|string, * IfNoneMatch?: null|string, * GrantFullControl?: null|string, * GrantRead?: null|string,