Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions Storage/src/Bucket.php
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@ public function exists(array $options = [])
* validation hash will be sent. Choose either `md5` or `crc32` to
* force a hash method regardless of performance implications.
* **Defaults to** `true`.
* @type string $crc32c The base64 encoded CRC32C checksum of the object
* data. If provided, this hash will be used for server-side
* validation.
* @type string $md5 The base64 encoded MD5 hash of the object data. If
* provided, this hash will be used for server-side validation.
* @type int $chunkSize If provided the upload will be done in chunks.
* The size must be in multiples of 262144 bytes. With chunking
* you have increased reliability at the risk of higher overhead.
Expand Down Expand Up @@ -379,6 +384,11 @@ public function upload($data, array $options = [])
* validation hash will be sent. Choose either `md5` or `crc32` to
* force a hash method regardless of performance implications.
* **Defaults to** `true`.
* @type string $crc32c The base64 encoded CRC32C checksum of the object
* data. If provided, this hash will be used for server-side
* validation.
* @type string $md5 The base64 encoded MD5 hash of the object data. If
* provided, this hash will be used for server-side validation.
* @type string $predefinedAcl Predefined ACL to apply to the object.
* Acceptable values include, `"authenticatedRead"`,
* `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`,
Expand Down
30 changes: 30 additions & 0 deletions Storage/src/Connection/Rest.php
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,36 @@ private function resolveUploadOptions(array $args)
$args['name'] = basename($args['data']->getMetadata('uri'));
}

if (isset($args['crc32c'])) {
$args['metadata']['crc32c'] = $args['crc32c'];
$userCrc32c = $args['crc32c'];
unset($args['crc32c']);
}
if (isset($args['md5'])) {
$args['metadata']['md5Hash'] = $args['md5'];
$userMd5 = $args['md5'];
unset($args['md5']);
}
if (isset($userCrc32c) || isset($userMd5)) {
// Disable auto-validation to prevent redundant calculations
$args['validate'] = false;

$xGoogHash = [];
if (isset($userMd5)) {
$xGoogHash[] = 'md5=' . $userMd5;
}
if (isset($userCrc32c)) {
$xGoogHash[] = 'crc32c=' . $userCrc32c;
}

// Append to existing X-Goog-Hash if present
if (isset($args['headers']['X-Goog-Hash'])) {
$args['headers']['X-Goog-Hash'] .= ',' . implode(',', $xGoogHash);
} else {
$args['headers']['X-Goog-Hash'] = implode(',', $xGoogHash);
}
}

$validate = $this->chooseValidationMethod($args);
$xGoogHashHeader = '';
if ($validate !== false) {
Expand Down
53 changes: 53 additions & 0 deletions Storage/tests/System/UploadObjectsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,57 @@ public function testCrc32cChecksumFails()
]
]);
}

public function testCrc32cChecksumFailsWithTopLevelOption()
{
$this->expectException(BadRequestException::class);

$data = 'somedata';
$badChecksum = base64_encode(hash('crc32c', 'bad-data', true));

self::$bucket->upload($data, [
'name' => uniqid(self::TESTING_PREFIX),
'crc32c' => $badChecksum
]);
}

public function testCrc32cChecksumSucceedsWithTopLevelOption()
{
$data = 'somedata';
$goodChecksum = base64_encode(hash('crc32c', $data, true));

$object = self::$bucket->upload($data, [
'name' => uniqid(self::TESTING_PREFIX),
'crc32c' => $goodChecksum
]);
$this->assertEquals(strlen($data), $object->info()['size']);
$object->delete();
}

public function testMd5ChecksumFailsWithTopLevelOption()
{
$this->expectException(BadRequestException::class);

$data = 'somedata';
$badChecksum = base64_encode(hash('md5', 'bad-data', true));

self::$bucket->upload($data, [
'name' => uniqid(self::TESTING_PREFIX),
'md5' => $badChecksum
]);
}

public function testMd5ChecksumSucceedsWithTopLevelOption()
{
$data = 'somedata';
$goodChecksum = base64_encode(hash('md5', $data, true));

$object = self::$bucket->upload($data, [
'name' => uniqid(self::TESTING_PREFIX),
'md5' => $goodChecksum
]);

$this->assertEquals(strlen($data), $object->info()['size']);
$object->delete();
}
}
134 changes: 134 additions & 0 deletions Storage/tests/Unit/Connection/RestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,140 @@ function ($args) use (&$actualRequest, $response) {
$this->assertArrayNotHasKey('crc32c', $metadata);
}

public function testInsertObjectWithUserProvidedHashes()
{
$rest = new Rest();
$testData = 'some test data';
$testStream = Utils::streamFor($testData);
$userCrc32c = 'user-crc';
$userMd5 = 'user-md5';
$expectedHashHeader = 'md5=' . $userMd5 . ',crc32c=' . $userCrc32c;

$actualRequest = null;
$response = new Response(200, ['Location' => 'http://www.mordor.com'], $this->successBody);

$this->requestWrapper->send(
Argument::type(RequestInterface::class),
Argument::type('array')
)->will(
function ($args) use (&$actualRequest, $response) {
$actualRequest = $args[0];
return $response;
}
);

$rest->setRequestWrapper($this->requestWrapper->reveal());

$options = [
'bucket' => 'my-test-bucket',
'name' => 'test-user-hash-file.txt',
'data' => $testStream,
'crc32c' => $userCrc32c,
'md5' => $userMd5,
'validate' => true
];

$uploader = $rest->insertObject($options);
$this->assertInstanceOf(MultipartUploader::class, $uploader);
$uploader->upload();

$this->assertNotNull($actualRequest);
$this->assertTrue($actualRequest->hasHeader('X-Goog-Hash'));
$this->assertEquals([$expectedHashHeader], $actualRequest->getHeader('X-Goog-Hash'));

list($contentType, $metadata) = $this->getContentTypeAndMetadata($actualRequest);
$this->assertEquals($userMd5, $metadata['md5Hash']);
$this->assertEquals($userCrc32c, $metadata['crc32c']);
}

public function testInsertObjectWithUserProvidedCrc32cOnly()
{
$rest = new Rest();
$testData = 'some test data';
$testStream = Utils::streamFor($testData);
$userCrc32c = 'user-crc';
$expectedHashHeader = 'crc32c=' . $userCrc32c;

$actualRequest = null;
$response = new Response(200, ['Location' => 'http://www.mordor.com'], $this->successBody);

$this->requestWrapper->send(
Argument::type(RequestInterface::class),
Argument::type('array')
)->will(
function ($args) use (&$actualRequest, $response) {
$actualRequest = $args[0];
return $response;
}
);

$rest->setRequestWrapper($this->requestWrapper->reveal());

$options = [
'bucket' => 'my-test-bucket',
'name' => 'test-user-hash-file.txt',
'data' => $testStream,
'crc32c' => $userCrc32c,
'validate' => true
];

$uploader = $rest->insertObject($options);
$this->assertInstanceOf(MultipartUploader::class, $uploader);
$uploader->upload();

$this->assertNotNull($actualRequest);
$this->assertTrue($actualRequest->hasHeader('X-Goog-Hash'));
$this->assertEquals([$expectedHashHeader], $actualRequest->getHeader('X-Goog-Hash'));

list($contentType, $metadata) = $this->getContentTypeAndMetadata($actualRequest);
$this->assertEquals($userCrc32c, $metadata['crc32c']);
$this->assertArrayNotHasKey('md5Hash', $metadata);
}

public function testInsertObjectWithUserProvidedMd5Only()
{
$rest = new Rest();
$testData = 'some test data';
$testStream = Utils::streamFor($testData);
$userMd5 = 'user-md5';
$expectedHashHeader = 'md5=' . $userMd5;

$actualRequest = null;
$response = new Response(200, ['Location' => 'http://www.mordor.com'], $this->successBody);

$this->requestWrapper->send(
Argument::type(RequestInterface::class),
Argument::type('array')
)->will(
function ($args) use (&$actualRequest, $response) {
$actualRequest = $args[0];
return $response;
}
);

$rest->setRequestWrapper($this->requestWrapper->reveal());

$options = [
'bucket' => 'my-test-bucket',
'name' => 'test-user-hash-file.txt',
'data' => $testStream,
'md5' => $userMd5,
'validate' => true
];

$uploader = $rest->insertObject($options);
$this->assertInstanceOf(MultipartUploader::class, $uploader);
$uploader->upload();

$this->assertNotNull($actualRequest);
$this->assertTrue($actualRequest->hasHeader('X-Goog-Hash'));
$this->assertEquals([$expectedHashHeader], $actualRequest->getHeader('X-Goog-Hash'));

list($contentType, $metadata) = $this->getContentTypeAndMetadata($actualRequest);
$this->assertEquals($userMd5, $metadata['md5Hash']);
$this->assertArrayNotHasKey('crc32c', $metadata);
}

/**
* @dataProvider validationMethod
*/
Expand Down
Loading