Skip to content
Open
3 changes: 3 additions & 0 deletions Core/src/Upload/MultipartUploader.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ private function prepareRequest()
$headers['Content-Length'] = $size;
}

$customHeaders = $this->requestOptions['restOptions']['headers'] ?? [];
$headers = array_merge($headers, $customHeaders);

return new Request(
'POST',
$this->uri,
Expand Down
10 changes: 10 additions & 0 deletions Core/src/Upload/ResumableUploader.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,16 @@ public function upload()
'Content-Range' => "bytes $rangeStart-$rangeEnd/$size",
];

$customHeaders = $this->requestOptions['restOptions']['headers'] ?? [];

// Check if this chunk is the final one
$isFinalChunk = ($size !== '*' && (int) ($rangeEnd + 1) === (int) $size);
if (!$isFinalChunk) {
unset($customHeaders['X-Goog-Hash']);
}

$headers = array_merge($headers, $customHeaders);

$request = new Request(
'PUT',
$resumeUri,
Expand Down
12 changes: 11 additions & 1 deletion Core/src/Upload/StreamableUploader.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ public function upload($writeSize = null)
return [];
}

$isFinalRequest = ($writeSize === null);

// find or create the resumeUri
$resumeUri = $this->getResumeUri();

if ($writeSize) {
if ($writeSize !== null) {
$rangeEnd = $this->rangeStart + $writeSize - 1;
$data = $this->data->read($writeSize);
} else {
Expand All @@ -62,6 +64,14 @@ public function upload($writeSize = null)
'Content-Range' => "bytes {$this->rangeStart}-$rangeEnd/*"
];

$customHeaders = $this->requestOptions['restOptions']['headers'] ?? [];

// Only include X-Goog-Hash if this is the final request
if (!$isFinalRequest) {
unset($customHeaders['X-Goog-Hash']);
}
$headers = array_merge($headers, $customHeaders);

$request = new Request(
'PUT',
$resumeUri,
Expand Down
42 changes: 42 additions & 0 deletions Core/tests/Unit/Upload/MultipartUploaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,48 @@ public function testUploadsAsyncData()
$actualPromise->wait()
);
}

public function testUploadsWithCustomHeaders()
{
$customHeaders = [
'X-Goog-Custom-Header' => 'custom-value',
'User-Agent' => 'custom-ua'
];

$requestWrapper = $this->prophesize(RequestWrapper::class);
$stream = Utils::streamFor('abcd');
$successBody = '{"canI":"kickIt"}';
$response = new Response(200, [], $successBody);

$requestWrapper->send(
Argument::that(function (RequestInterface $request) use ($customHeaders) {
foreach ($customHeaders as $key => $value) {
if ($request->getHeaderLine($key) !== $value) {
return false;
}
}

$contentType = $request->getHeaderLine('Content-Type');
return str_contains($contentType, 'multipart/related')
&& str_contains($contentType, 'boundary=boundary');
}),
Argument::type('array')
)->willReturn($response);

$uploader = new MultipartUploader(
$requestWrapper->reveal(),
$stream,
'http://www.example.com',
[
'restOptions' => [
'headers' => $customHeaders
]
]
);

$this->assertEquals(json_decode($successBody, true), $uploader->upload());
}

/**
* @dataProvider streamSizes
*/
Expand Down
113 changes: 113 additions & 0 deletions Core/tests/Unit/Upload/ResumableUploaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,119 @@ public function testRetryOptionsPassing()
$this->assertTrue($retryListenerCalled);
}

public function testUploadSendsGoogHashOnFinalChunk()
{
$hashValue = 'crc32c=abc123';
$resumeUri = 'http://some-resume-uri.example.com';

$this->requestWrapper->send(
Argument::which('getMethod', 'POST'),
Argument::type('array')
)->willReturn(new Response(200, ['Location' => $resumeUri]));

$this->requestWrapper->send(
Argument::that(function (RequestInterface $request) use ($hashValue) {
return $request->getHeaderLine('X-Goog-Hash') === $hashValue;
}),
Argument::type('array')
)->willReturn(new Response(200, [], $this->successBody));

$uploader = new ResumableUploader(
$this->requestWrapper->reveal(),
$this->stream,
'http://www.example.com',
[
'restOptions' => [
'headers' => ['X-Goog-Hash' => $hashValue]
]
]
);

$this->assertEquals(json_decode($this->successBody, true), $uploader->upload());
}

public function testUploadDoesNotSendGoogHashOnIntermediateChunk()
{
$hashValue = 'crc32c=abc123';
$resumeUri = 'http://some-resume-uri.example.com';

$this->requestWrapper->send(
Argument::which('getMethod', 'POST'),
Argument::type('array')
)->willReturn(new Response(200, ['Location' => $resumeUri]));

$this->requestWrapper->send(
Argument::that(function (RequestInterface $request) {
return $request->getHeaderLine('Content-Range') === 'bytes 0-1/4'
&& !$request->hasHeader('X-Goog-Hash');
}),
Argument::type('array')
)->willReturn(new Response(308, ['Range' => 'bytes 0-1']));

$this->requestWrapper->send(
Argument::that(function (RequestInterface $request) {
return $request->getHeaderLine('Content-Range') === 'bytes 2-3/4';
}),
Argument::type('array')
)->willReturn(new Response(200, [], $this->successBody));

$uploader = new ResumableUploader(
$this->requestWrapper->reveal(),
$this->stream, // size 4
'http://www.example.com',
[
'chunkSize' => 2, // Force multi-chunk upload
'restOptions' => [
'headers' => ['X-Goog-Hash' => $hashValue]
]
]
);

$this->assertEquals(json_decode($this->successBody, true), $uploader->upload());
}

public function testGoogHashOnlyOnFinalChunkOfMultiChunkUpload()
{
$hashValue = 'crc32c=abc123';
$resumeUri = 'http://some-resume-uri.example.com';
$this->stream = Utils::streamFor('01234567'); // 8 bytes total

$this->requestWrapper->send(
Argument::which('getMethod', 'POST'),
Argument::type('array')
)->willReturn(new Response(200, ['Location' => $resumeUri]));

$this->requestWrapper->send(
Argument::that(function (RequestInterface $request) {
return $request->getHeaderLine('Content-Range') === 'bytes 0-3/8'
&& !$request->hasHeader('X-Goog-Hash');
}),
Argument::type('array')
)->willReturn(new Response(308, ['Range' => 'bytes 0-3']));

$this->requestWrapper->send(
Argument::that(function (RequestInterface $request) use ($hashValue) {
return $request->getHeaderLine('Content-Range') === 'bytes 4-7/8'
&& $request->getHeaderLine('X-Goog-Hash') === $hashValue;
}),
Argument::type('array')
)->willReturn(new Response(200, [], $this->successBody));

$uploader = new ResumableUploader(
$this->requestWrapper->reveal(),
$this->stream,
'http://www.example.com',
[
'chunkSize' => 4,
'restOptions' => [
'headers' => ['X-Goog-Hash' => $hashValue]
]
]
);

$this->assertEquals(json_decode($this->successBody, true), $uploader->upload());
}

public function testThrowsExceptionWhenAttemptsAsyncUpload()
{
$this->expectException(GoogleException::class);
Expand Down
109 changes: 109 additions & 0 deletions Core/tests/Unit/Upload/StreamableUploaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,115 @@ public function testLastChunkSendsCorrectHeaders()
$uploader->upload();
}

public function testUploadWithCustomGoogHashHeader()
{
$hashValue = 'md5=abc123';
$resumeUri = 'http://some-resume-uri.example.com';

$resumeResponse = new Response(200, ['Location' => $resumeUri]);
$this->requestWrapper->send(
Argument::type(RequestInterface::class),
Argument::any()
)->willReturn($resumeResponse)->shouldBeCalled();

$uploadResponse = new Response(200, ['Location' => $resumeUri], $this->successBody);
$this->requestWrapper->send(
Argument::that(function ($request) use ($resumeUri, $hashValue) {
return (string) $request->getUri() === $resumeUri
&& $request->getHeaderLine('X-Goog-Hash') === $hashValue;
}),
Argument::any()
)->willReturn($uploadResponse)->shouldBeCalled();

$uploader = new StreamableUploader(
$this->requestWrapper->reveal(),
$this->stream,
'http://www.example.com',
[
'restOptions' => [
'headers' => [
'X-Goog-Hash' => $hashValue,
'Other-Header' => 'should-be-ignored'
]
]
]
);

$this->stream->write("some data");

$this->assertEquals(json_decode($this->successBody, true), $uploader->upload());
}

public function testUploadDoesNotSendGoogHashWhenConditionNotMet()
{
$hashValue = 'md5=abc123';
$resumeUri = 'http://some-resume-uri.example.com';

$resumeResponse = new Response(200, ['Location' => $resumeUri]);
$this->requestWrapper->send(
Argument::which('getMethod', 'POST'),
Argument::type('array')
)->willReturn($resumeResponse);

$uploadResponse = new Response(200, [], $this->successBody);
$this->requestWrapper->send(
Argument::that(function (RequestInterface $request) {
return !$request->hasHeader('X-Goog-Hash');
}),
Argument::type('array')
)->willReturn($uploadResponse);

$uploader = new StreamableUploader(
$this->requestWrapper->reveal(),
$this->stream,
'http://www.example.com',
[
'restOptions' => [
'headers' => ['X-Goog-Hash' => $hashValue]
]
]
);

$this->stream->write("0123456789ABCDEF");

$this->assertEquals(json_decode($this->successBody, true), $uploader->upload(16));
}

public function testUploadSendsGoogHashOnFinalStep()
{
$hashValue = 'md5=finalHash';
$resumeUri = 'http://some-resume-uri.example.com';

$resumeResponse = new Response(200, ['Location' => $resumeUri]);
$this->requestWrapper->send(
Argument::which('getMethod', 'POST'),
Argument::type('array')
)->willReturn($resumeResponse)->shouldBeCalled();

$uploadResponse = new Response(200, [], $this->successBody);
$this->requestWrapper->send(
Argument::that(function (RequestInterface $request) use ($hashValue) {
return $request->getHeaderLine('X-Goog-Hash') === $hashValue;
}),
Argument::type('array')
)->willReturn($uploadResponse)->shouldBeCalled();

$uploader = new StreamableUploader(
$this->requestWrapper->reveal(),
$this->stream,
'http://www.example.com',
[
'restOptions' => [
'headers' => ['X-Goog-Hash' => $hashValue]
]
]
);

$this->stream->write("final data");

$this->assertEquals(json_decode($this->successBody, true), $uploader->upload());
}

public function testThrowsExceptionWhenAttemptsAsyncUpload()
{
$this->expectException(GoogleException::class);
Expand Down
Loading
Loading