Skip to content

Commit 308667e

Browse files
committed
feat: add default end-to-end checksumming for JournalingBlobWriteSessionConfig
Create TestUtils.rmDashRm to recursively delete a directory, and update ITSyncAndUploadUnbufferedWritableByteChannelPropertyTest to use it. Add TmpDir auto closable to allow managed lifecycle of a temporary directory in a test. Remove md5Base64 from ChecksummedTestContent#toString(). Base64 values can contain path values that are not valid for filesystems. Add ITObjectChecksumSupportTest for journaling uploads. Update ITSyncAndUploadUnbufferedWritableByteChannelPropertyTest property tests to enable and expect crc32c values in all messages.
1 parent 4e64a8b commit 308667e

6 files changed

Lines changed: 181 additions & 24 deletions

File tree

google-cloud-storage/src/main/java/com/google/cloud/storage/JournalingBlobWriteSessionConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ public WritableByteChannelSession<?, BlobInfo> writeSession(
201201
ResumableMedia.gapic()
202202
.write()
203203
.byteChannel(write)
204-
.setHasher(Hasher.noop())
204+
.setHasher(opts.getHasher())
205205
.setByteStringStrategy(ByteStringStrategy.copy())
206206
.journaling()
207207
.withRetryConfig(

google-cloud-storage/src/test/java/com/google/cloud/storage/ITSyncAndUploadUnbufferedWritableByteChannelPropertyTest.java

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import com.google.protobuf.ByteString;
4848
import com.google.protobuf.Message;
4949
import com.google.protobuf.TextFormat;
50+
import com.google.storage.v2.ChecksummedData;
5051
import com.google.storage.v2.Object;
5152
import com.google.storage.v2.ObjectChecksums;
5253
import com.google.storage.v2.QueryWriteStatusRequest;
@@ -63,11 +64,8 @@
6364
import io.grpc.stub.StreamObserver;
6465
import java.io.IOException;
6566
import java.nio.ByteBuffer;
66-
import java.nio.file.FileVisitResult;
6767
import java.nio.file.Files;
6868
import java.nio.file.Path;
69-
import java.nio.file.SimpleFileVisitor;
70-
import java.nio.file.attribute.BasicFileAttributes;
7169
import java.util.ArrayDeque;
7270
import java.util.ArrayList;
7371
import java.util.Collections;
@@ -124,23 +122,7 @@ static void beforeContainer() throws IOException {
124122
@AfterContainer
125123
static void afterContainer() throws IOException {
126124
if (tmpFolder != null) {
127-
Files.walkFileTree(
128-
tmpFolder,
129-
new SimpleFileVisitor<Path>() {
130-
@Override
131-
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
132-
throws IOException {
133-
Files.deleteIfExists(file);
134-
return FileVisitResult.CONTINUE;
135-
}
136-
137-
@Override
138-
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
139-
throws IOException {
140-
Files.deleteIfExists(dir);
141-
return FileVisitResult.CONTINUE;
142-
}
143-
});
125+
TestUtils.rmDashRf(tmpFolder);
144126
}
145127
}
146128

@@ -754,7 +736,7 @@ public static Scenario of(
754736
.toString(),
755737
objectName,
756738
objectSize,
757-
new ChunkSegmenter(Hasher.noop(), ByteStringStrategy.copy(), segmentSize, quantum),
739+
new ChunkSegmenter(Hasher.enabled(), ByteStringStrategy.copy(), segmentSize, quantum),
758740
BufferHandle.allocate(segmentSize),
759741
BufferHandle.allocate(segmentSize),
760742
failuresQueue,
@@ -1011,6 +993,25 @@ public FailureInducingWriteObjectRequestObserver(
1011993

1012994
@Override
1013995
public void onNext(WriteObjectRequest writeObjectRequest) {
996+
if (writeObjectRequest.hasChecksummedData()) {
997+
ChecksummedData checksummedData = writeObjectRequest.getChecksummedData();
998+
if (!checksummedData.hasCrc32C()) {
999+
errored = true;
1000+
sendFailure("no crc32c value specified");
1001+
return;
1002+
}
1003+
if (!checksummedData.getContent().isEmpty() && checksummedData.getCrc32C() == 0) {
1004+
errored = true;
1005+
sendFailure("crc32c value of 0 with non-empty content");
1006+
return;
1007+
}
1008+
}
1009+
if (writeObjectRequest.hasObjectChecksums()
1010+
&& !writeObjectRequest.getObjectChecksums().hasCrc32C()) {
1011+
errored = true;
1012+
sendFailure("missing object_checksums.crc32c");
1013+
return;
1014+
}
10141015
if (ctx == null) {
10151016
UploadId uploadId = UploadId.of(writeObjectRequest.getUploadId());
10161017
if (data.containsKey(uploadId)) {
@@ -1053,6 +1054,11 @@ public void onCompleted() {
10531054
responseObserver.onNext(resp);
10541055
responseObserver.onCompleted();
10551056
}
1057+
1058+
private void sendFailure(String description) {
1059+
responseObserver.onError(
1060+
Code.INVALID_ARGUMENT.toStatus().withDescription(description).asRuntimeException());
1061+
}
10561062
}
10571063

10581064
@FunctionalInterface

google-cloud-storage/src/test/java/com/google/cloud/storage/TestUtils.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@
5353
import java.nio.Buffer;
5454
import java.nio.ByteBuffer;
5555
import java.nio.charset.StandardCharsets;
56+
import java.nio.file.FileVisitResult;
57+
import java.nio.file.Path;
58+
import java.nio.file.SimpleFileVisitor;
59+
import java.nio.file.attribute.BasicFileAttributes;
5660
import java.util.Arrays;
5761
import java.util.Collections;
5862
import java.util.HashMap;
@@ -374,4 +378,23 @@ private static String messagesToText(Throwable t, String indent) {
374378
.flatMap(s -> s)
375379
.collect(Collectors.joining("\n"));
376380
}
381+
382+
public static void rmDashRf(Path path) throws IOException {
383+
java.nio.file.Files.walkFileTree(
384+
path,
385+
new SimpleFileVisitor<Path>() {
386+
@Override
387+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
388+
throws IOException {
389+
java.nio.file.Files.deleteIfExists(file);
390+
return FileVisitResult.CONTINUE;
391+
}
392+
393+
@Override
394+
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
395+
java.nio.file.Files.deleteIfExists(dir);
396+
return FileVisitResult.CONTINUE;
397+
}
398+
});
399+
}
377400
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.storage;
18+
19+
import com.google.common.base.MoreObjects;
20+
import java.io.IOException;
21+
import java.nio.file.Files;
22+
import java.nio.file.Path;
23+
import org.slf4j.Logger;
24+
import org.slf4j.LoggerFactory;
25+
26+
public final class TmpDir implements AutoCloseable {
27+
private static final Logger LOGGER = LoggerFactory.getLogger(TmpDir.class);
28+
29+
private final Path path;
30+
31+
private TmpDir(Path path) {
32+
this.path = path;
33+
}
34+
35+
public Path getPath() {
36+
return path;
37+
}
38+
39+
/** Delete the TmpFile this handle is holding */
40+
@Override
41+
public void close() throws IOException {
42+
TestUtils.rmDashRf(path);
43+
}
44+
45+
@Override
46+
public String toString() {
47+
return MoreObjects.toStringHelper(this).add("path", path).toString();
48+
}
49+
50+
/**
51+
* Create a temporary file, which will be deleted when close is called on the returned {@link
52+
* TmpDir}
53+
*/
54+
public static TmpDir of(Path baseDir, String prefix) throws IOException {
55+
LOGGER.trace("of(baseDir : {}, prefix : {})", baseDir, prefix);
56+
Path path = Files.createTempDirectory(baseDir, prefix);
57+
return new TmpDir(path);
58+
}
59+
}

google-cloud-storage/src/test/java/com/google/cloud/storage/it/ChecksummedTestContent.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,7 @@ public List<ChecksummedTestContent> chunkup(int chunkSize) {
115115
public String toString() {
116116
return MoreObjects.toStringHelper(this)
117117
.add("byteCount", bytes.length)
118-
.add("crc32c", crc32c)
119-
.add("md5Base64", md5Base64)
118+
.add("crc32c", Integer.toUnsignedString(crc32c))
120119
.toString();
121120
}
122121

google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectChecksumSupportTest.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.google.cloud.storage.Storage.BlobWriteOption;
3232
import com.google.cloud.storage.StorageException;
3333
import com.google.cloud.storage.StorageOptions;
34+
import com.google.cloud.storage.TmpDir;
3435
import com.google.cloud.storage.TmpFile;
3536
import com.google.cloud.storage.TransportCompatibility.Transport;
3637
import com.google.cloud.storage.it.ITObjectChecksumSupportTest.ChecksummedTestContentProvider;
@@ -54,7 +55,9 @@
5455
import java.nio.file.Path;
5556
import java.nio.file.Paths;
5657
import java.util.concurrent.TimeUnit;
58+
import org.junit.Rule;
5759
import org.junit.Test;
60+
import org.junit.rules.TestName;
5861
import org.junit.runner.RunWith;
5962

6063
@RunWith(StorageITRunner.class)
@@ -75,6 +78,8 @@ public final class ITObjectChecksumSupportTest {
7578

7679
@Parameter public ChecksummedTestContent content;
7780

81+
@Rule public final TestName testName = new TestName();
82+
7883
public static final class ChecksummedTestContentProvider implements ParametersProvider {
7984

8085
@Override
@@ -351,4 +356,69 @@ public void testCrc32cValidated_bidiWrite_expectFailure() throws Exception {
351356
assertThat(expected.getCode()).isEqualTo(400);
352357
}
353358
}
359+
360+
@Test
361+
@CrossRun.Exclude(transports = Transport.HTTP)
362+
public void testCrc32cValidated_journaling_expectSuccess() throws Exception {
363+
String blobName = generator.randomObjectName();
364+
BlobId blobId = BlobId.of(bucket.getName(), blobName);
365+
BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setCrc32c(content.getCrc32cBase64()).build();
366+
367+
byte[] bytes = content.getBytes();
368+
369+
try (TmpDir journalingDir = TmpDir.of(tmpDir, testName.getMethodName())) {
370+
StorageOptions options =
371+
this.storage.getOptions().toBuilder()
372+
.setBlobWriteSessionConfig(
373+
BlobWriteSessionConfigs.journaling(ImmutableList.of(journalingDir.getPath())))
374+
.build();
375+
376+
try (Storage storage = options.getService()) {
377+
BlobWriteSession session =
378+
storage.blobWriteSession(
379+
blobInfo, BlobWriteOption.doesNotExist(), BlobWriteOption.crc32cMatch());
380+
381+
try (ReadableByteChannel src = Channels.newChannel(new ByteArrayInputStream(bytes));
382+
WritableByteChannel dst = session.open()) {
383+
ByteStreams.copy(src, dst);
384+
}
385+
386+
BlobInfo gen1 = session.getResult().get(5, TimeUnit.SECONDS);
387+
assertThat(gen1.getCrc32c()).isEqualTo(content.getCrc32cBase64());
388+
}
389+
}
390+
}
391+
392+
@Test
393+
@CrossRun.Exclude(transports = Transport.HTTP)
394+
public void testCrc32cValidated_journaling_expectFailure() throws Exception {
395+
String blobName = generator.randomObjectName();
396+
BlobId blobId = BlobId.of(bucket.getName(), blobName);
397+
BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setCrc32c(content.getCrc32cBase64()).build();
398+
399+
byte[] bytes = content.concat('x');
400+
401+
try (TmpDir journalingDir = TmpDir.of(tmpDir, generator.randomObjectName())) {
402+
StorageOptions options =
403+
this.storage.getOptions().toBuilder()
404+
.setBlobWriteSessionConfig(
405+
BlobWriteSessionConfigs.journaling(ImmutableList.of(journalingDir.getPath())))
406+
.build();
407+
408+
try (Storage storage = options.getService()) {
409+
BlobWriteSession session =
410+
storage.blobWriteSession(
411+
blobInfo, BlobWriteOption.doesNotExist(), BlobWriteOption.crc32cMatch());
412+
413+
WritableByteChannel dst = session.open();
414+
try (ReadableByteChannel src = Channels.newChannel(new ByteArrayInputStream(bytes))) {
415+
ByteStreams.copy(src, dst);
416+
}
417+
418+
StorageException expected = assertThrows(StorageException.class, dst::close);
419+
420+
assertThat(expected.getCode()).isEqualTo(400);
421+
}
422+
}
423+
}
354424
}

0 commit comments

Comments
 (0)