Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
7 changes: 7 additions & 0 deletions .changes/nextrelease/existence-methods.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"type": "feature",
"category": "S3",
"description": "Adds DoesBucketExistV2 and DoesObjectExistV2 helper methods"
}
]
39 changes: 39 additions & 0 deletions src/S3/S3ClientInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use Aws\AwsClientInterface;
use Aws\CommandInterface;
use Aws\ResultInterface;
use Aws\S3\Exception\S3Exception;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\RequestInterface;

Expand Down Expand Up @@ -40,6 +41,8 @@ public function createPresignedRequest(CommandInterface $command, $expires, arra
public function getObjectUrl($bucket, $key);

/**
* @deprecated Use doesBucketExistV2() instead
*
* Determines whether or not a bucket exists by name.
*
* @param string $bucket The name of the bucket
Expand All @@ -49,6 +52,24 @@ public function getObjectUrl($bucket, $key);
public function doesBucketExist($bucket);

/**
* Determines whether or not a bucket exists by name. This method uses S3's
* HeadBucket operation and requires the relevant bucket permissions in the
* default case to avoid inaccuracies.
*
* @param string $bucket The name of the bucket
* @param bool $accept403 Set to true for this method to return true in the case of
* invalid bucket-level permissions. Credentials MUST be valid
* to avoid inaccuracies. Using the default value of false will
* cause an exception to be thrown instead.
*
* @return bool
* @throws S3Exception|Exception if there is an unhandled exception
*/
public function doesBucketExistV2($bucket, $accept403);

/**
* @deprecated Use doesObjectExistV2() instead
*
* Determines whether or not an object exists by name.
*
* @param string $bucket The name of the bucket
Expand All @@ -60,6 +81,24 @@ public function doesBucketExist($bucket);
*/
public function doesObjectExist($bucket, $key, array $options = []);

/**
* Determines whether or not an object exists by name. This method uses S3's HeadObject
* operation and requires the relevant bucket and object permissions to avoid inaccuracies.
*
* @param string $bucket The name of the bucket
* @param string $key The key of the object
* @param bool $includeDeleteMarkers Set to true to consider delete markers
* existing objects. Using the default value
* of false will ignore delete markers and
* return false.
* @param array $options Additional options available in the HeadObject
* operation (e.g., VersionId).
*
* @return bool
* @throws S3Exception|Exception if there is an unhandled exception
*/
public function doesObjectExistV2($bucket, $key, $includeDeleteMarkers, array $options = []);

/**
* Register the Amazon S3 stream wrapper with this client instance.
*/
Expand Down
63 changes: 63 additions & 0 deletions src/S3/S3ClientTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Aws\Exception\AwsException;
use Aws\HandlerList;
use Aws\ResultInterface;
use Aws\S3\Exception\PermanentRedirectException;
use Aws\S3\Exception\S3Exception;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Promise\RejectedPromise;
Expand Down Expand Up @@ -260,6 +261,30 @@ public function doesBucketExist($bucket)
);
}

/**
* @see S3ClientInterface::doesBucketExistV2()
*/
public function doesBucketExistV2($bucket, $accept403 = false)
{
$command = $this->getCommand('HeadBucket', ['Bucket' => $bucket]);

try {
$this->execute($command);
return true;
} catch (S3Exception $e) {
Comment thread
stobrien89 marked this conversation as resolved.
if (
($accept403 && $e->getStatusCode() === 403)
|| $e instanceof PermanentRedirectException
) {
return true;
}
if ($e->getStatusCode() === 404) {
return false;
}
throw $e;
}
}

/**
* @see S3ClientInterface::doesObjectExist()
*/
Expand All @@ -273,6 +298,44 @@ public function doesObjectExist($bucket, $key, array $options = [])
);
}

/**
* @see S3ClientInterface::doesObjectExistV2()
*/
public function doesObjectExistV2(
$bucket,
$key,
$includeDeleteMarkers = false,
array $options = []
){
$command = $this->getCommand('HeadObject', [
'Bucket' => $bucket,
'Key' => $key
] + $options
);

try {
$this->execute($command);
return true;
} catch (S3Exception $e) {
if ($includeDeleteMarkers
&& $this->useDeleteMarkers($e)
) {
return true;
}
if ($e->getStatusCode() === 404) {
return false;
}
throw $e;
}
}

private function useDeleteMarkers($exception)
{
$response = $exception->getResponse();
return !empty($response)
&& $response->getHeader('x-amz-delete-marker');
}

/**
* Determines whether or not a resource exists using a command
*
Expand Down
73 changes: 63 additions & 10 deletions tests/S3/S3ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Aws\Endpoint\PartitionEndpointProvider;
use Aws\Middleware;
use Aws\Result;
use Aws\S3\Exception\PermanentRedirectException;
use Aws\S3\Exception\S3Exception;
use Aws\S3\RegionalEndpoint\Configuration;
use Aws\S3\S3Client;
Expand Down Expand Up @@ -232,6 +233,13 @@ public function testRegistersStreamWrapper()

public function doesExistProvider()
{
$redirectException = new PermanentRedirectException(
'',
new Command('mockCommand'),
['response' => new Response(301)]
);
$deleteMarkerMock = $this->getS3ErrorMock('Foo', 404, true);

return [
['foo', null, true, []],
['foo', 'bar', true, []],
Expand All @@ -241,31 +249,67 @@ public function doesExistProvider()
['foo', 'bar', false, $this->getS3ErrorMock('Foo', 401)],
['foo', null, -1, $this->getS3ErrorMock('Foo', 500)],
['foo', 'bar', -1, $this->getS3ErrorMock('Foo', 500)],
['foo', null, true, [], true],
['foo', 'bar', true, [] , true],
['foo', null, false, $this->getS3ErrorMock('Foo', 404), true],
['foo', 'bar', false, $this->getS3ErrorMock('Foo', 404), true],
['foo', null, -1, $this->getS3ErrorMock('Forbidden', 403), true],
['foo', 'bar', -1, $this->getS3ErrorMock('Forbidden', 403), true],
['foo', null, true, $this->getS3ErrorMock('Forbidden', 403), true, true],
['foo', 'bar', true, $deleteMarkerMock, true, false, true],
['foo', 'bar', false, $deleteMarkerMock, true, false, false],
['foo', null, true, $redirectException, true],
];
}

private function getS3ErrorMock($errCode, $statusCode)
private function getS3ErrorMock(
$errCode,
$statusCode,
$deleteMarker = false
)
{
$response = new Response($statusCode);
$deleteMarker && $response = $response->withHeader(
'x-amz-delete-marker',
'true'
);

$context = [
'code' => $errCode,
'response' => new Response($statusCode),
'response' => $response,
];
return new S3Exception('', new Command('mockCommand'), $context);
}

/**
* @dataProvider doesExistProvider
*/
public function testsIfExists($bucket, $key, $exists, $result)
public function testsIfExists(
$bucket,
$key,
$exists,
$result,
$V2 = false,
$accept403 = false,
$acceptDeleteMarkers = false
)
{
/** @var S3Client $s3 */
$s3 = $this->getTestClient('S3', ['region' => 'us-east-1']);
$this->addMockResults($s3, [$result]);
try {
if ($key) {
$this->assertSame($exists, $s3->doesObjectExist($bucket, $key));
} else {
$this->assertSame($exists, $s3->doesBucketExist($bucket));
if ($V2) {
if ($key) {
$this->assertSame($exists, $s3->doesObjectExistV2($bucket, $key, $acceptDeleteMarkers));
} else {
$this->assertSame($exists, $s3->doesBucketExistV2($bucket, $accept403));
}
}else {
if ($key) {
$this->assertSame($exists, $s3->doesObjectExist($bucket, $key));
} else {
$this->assertSame($exists, $s3->doesBucketExist($bucket));
}
}
} catch (\Exception $e) {
$this->assertSame(-1, $exists);
Expand All @@ -278,7 +322,10 @@ public function testReturnsObjectUrl()
'region' => 'us-east-1',
'credentials' => false
]);
$this->assertSame('https://foo.s3.amazonaws.com/bar', $s3->getObjectUrl('foo', 'bar'));
$this->assertSame(
'https://foo.s3.amazonaws.com/bar',
$s3->getObjectUrl('foo', 'bar')
);
}

public function testReturnsObjectUrlWithPathStyleFallback()
Expand All @@ -287,7 +334,10 @@ public function testReturnsObjectUrlWithPathStyleFallback()
'region' => 'us-east-1',
'credentials' => false,
]);
$this->assertSame('https://s3.amazonaws.com/foo.baz/bar', $s3->getObjectUrl('foo.baz', 'bar'));
$this->assertSame(
'https://s3.amazonaws.com/foo.baz/bar',
$s3->getObjectUrl('foo.baz', 'bar')
);
}

public function testReturnsObjectUrlWithPathStyle()
Expand All @@ -297,7 +347,10 @@ public function testReturnsObjectUrlWithPathStyle()
'credentials' => false,
'use_path_style_endpoint' => true
]);
$this->assertSame('https://s3.amazonaws.com/foo/bar', $s3->getObjectUrl('foo', 'bar'));
$this->assertSame(
'https://s3.amazonaws.com/foo/bar',
$s3->getObjectUrl('foo', 'bar')
);
}

public function testReturnsObjectUrlViaPath()
Expand Down