JCLOUDS-912: JCLOUDS-1547: GCS InputStream single-part upload

Previously this provider worked around a RestAnnotationProcessor quirk
by using multi-part uploads for InputStream payloads.  Instead work
around the quirk another way which allows a single-part upload.  This
allows inclusion of the Content-MD5 header during object creation.
Backfill tests with both ByteSource and InputStream inputs.
This commit is contained in:
Andrew Gaul 2020-05-23 13:25:24 +09:00 committed by Andrew Gaul
parent 08a16c95fb
commit 6e6f8ebf77
6 changed files with 53 additions and 11 deletions

View File

@ -267,7 +267,7 @@ public class BaseBlobIntegrationTest extends BaseBlobStoreIntegrationTest {
}
}
private void putBlobWithMd5(byte[] payload, HashCode contentMD5) throws InterruptedException, IOException {
private void putBlobWithMd5(Payload payload, long contentLength, HashCode contentMD5) throws InterruptedException, IOException {
String container = getContainerName();
BlobStore blobStore = view.getBlobStore();
try {
@ -275,6 +275,7 @@ public class BaseBlobIntegrationTest extends BaseBlobStoreIntegrationTest {
Blob blob = blobStore
.blobBuilder(blobName)
.payload(payload)
.contentLength(contentLength)
.contentMD5(contentMD5)
.build();
blobStore.putBlob(container, blob);
@ -288,18 +289,39 @@ public class BaseBlobIntegrationTest extends BaseBlobStoreIntegrationTest {
}
@Test(groups = { "integration", "live" })
public void testPutCorrectContentMD5() throws InterruptedException, IOException {
byte[] payload = createTestInput(1024).read();
HashCode contentMD5 = md5().hashBytes(payload);
putBlobWithMd5(payload, contentMD5);
public void testPutCorrectContentMD5ByteSource() throws InterruptedException, IOException {
ByteSource payload = createTestInput(1024);
HashCode contentMD5 = md5().hashBytes(payload.read());
putBlobWithMd5(new ByteSourcePayload(payload), payload.size(), contentMD5);
}
@Test(groups = { "integration", "live" })
public void testPutIncorrectContentMD5() throws InterruptedException, IOException {
byte[] payload = createTestInput(1024).read();
public void testPutIncorrectContentMD5ByteSource() throws InterruptedException, IOException {
ByteSource payload = createTestInput(1024);
HashCode contentMD5 = md5().hashBytes(new byte[0]);
try {
putBlobWithMd5(payload, contentMD5);
putBlobWithMd5(new ByteSourcePayload(payload), payload.size(), contentMD5);
fail();
} catch (HttpResponseException hre) {
if (hre.getResponse().getStatusCode() != getIncorrectContentMD5StatusCode()) {
throw hre;
}
}
}
@Test(groups = { "integration", "live" })
public void testPutCorrectContentMD5InputStream() throws InterruptedException, IOException {
ByteSource payload = createTestInput(1024);
HashCode contentMD5 = md5().hashBytes(payload.read());
putBlobWithMd5(new InputStreamPayload(payload.openStream()), payload.size(), contentMD5);
}
@Test(groups = { "integration", "live" })
public void testPutIncorrectContentMD5InputStream() throws InterruptedException, IOException {
ByteSource payload = createTestInput(1024);
HashCode contentMD5 = md5().hashBytes(new byte[0]);
try {
putBlobWithMd5(new InputStreamPayload(payload.openStream()), payload.size(), contentMD5);
fail();
} catch (HttpResponseException hre) {
if (hre.getResponse().getStatusCode() != getIncorrectContentMD5StatusCode()) {

View File

@ -97,6 +97,14 @@ public class PayloadEnclosingImpl implements PayloadEnclosing {
setPayload(newPayload(checkNotNull(data, "data")));
}
@Override
public void resetPayload(boolean release) {
if (release && payload != null) {
payload.release();
}
payload = null;
}
@Override
public int hashCode() {
final int prime = 31;

View File

@ -48,4 +48,5 @@ public interface PayloadEnclosing {
@Nullable
Payload getPayload();
void resetPayload(boolean release);
}

View File

@ -80,9 +80,19 @@ public final class B2BlobIntegrationLiveTest extends BaseBlobIntegrationTest {
}
@Override
public void testPutIncorrectContentMD5() throws InterruptedException, IOException {
public void testPutIncorrectContentMD5ByteSource() throws InterruptedException, IOException {
try {
super.testPutIncorrectContentMD5();
super.testPutIncorrectContentMD5ByteSource();
failBecauseExceptionWasNotThrown(AssertionError.class);
} catch (AssertionError ae) {
throw new SkipException("B2 does not enforce Content-MD5", ae);
}
}
@Override
public void testPutIncorrectContentMD5InputStream() throws InterruptedException, IOException {
try {
super.testPutIncorrectContentMD5InputStream();
failBecauseExceptionWasNotThrown(AssertionError.class);
} catch (AssertionError ae) {
throw new SkipException("B2 does not enforce Content-MD5", ae);

View File

@ -57,6 +57,7 @@ public final class MultipartUploadBinder implements MapBinder {
Part jsonPart = Part.create("Metadata", jsonPayload, new Part.PartOptions().contentType(APPLICATION_JSON));
Part mediaPart = Part.create(template.name(), payload, new Part.PartOptions().contentType(contentType));
request.resetPayload(/*release=*/ false);
request.setPayload(new MultipartForm(BOUNDARY_HEADER, jsonPart, mediaPart));
// HeaderPart
request.toBuilder().replaceHeader(CONTENT_TYPE, "Multipart/related; boundary= " + BOUNDARY_HEADER).build();

View File

@ -211,7 +211,7 @@ public final class GoogleCloudStorageBlobStore extends BaseBlobStore {
public String putBlob(String container, Blob blob, PutOptions options) {
long length = checkNotNull(blob.getPayload().getContentMetadata().getContentLength());
if (length != 0 && (options.isMultipart() || !blob.getPayload().isRepeatable())) {
if (length != 0 && options.isMultipart()) {
// JCLOUDS-912 prevents using single-part uploads with InputStream payloads.
// Work around this with multi-part upload which buffers parts in-memory.
return putMultipartBlob(container, blob, options);