nonDeleteRules =
@@ -3513,6 +3533,7 @@ private Builder clearDeleteLifecycleRules() {
customerManagedEncryptionEnforcementConfig = builder.customerManagedEncryptionEnforcementConfig;
customerSuppliedEncryptionEnforcementConfig =
builder.customerSuppliedEncryptionEnforcementConfig;
+ isUnreachable = builder.isUnreachable;
modifiedFields = builder.modifiedFields.build();
}
@@ -3886,6 +3907,16 @@ public HierarchicalNamespace getHierarchicalNamespace() {
return customerSuppliedEncryptionEnforcementConfig;
}
+ /**
+ * Returns a {@code Boolean} with {@code true} if the bucket is unreachable, else {@code null}
+ *
+ * A bucket may be unreachable if the region in which it resides is experiencing an outage or
+ * if there are other temporary access issues.
+ */
+ public Boolean isUnreachable() {
+ return Data.isNull(isUnreachable) ? null : isUnreachable;
+ }
+
/** Returns a builder for the current bucket. */
public Builder toBuilder() {
return new BuilderImpl(this);
@@ -3931,7 +3962,8 @@ public int hashCode() {
ipFilter,
googleManagedEncryptionEnforcementConfig,
customerManagedEncryptionEnforcementConfig,
- customerSuppliedEncryptionEnforcementConfig);
+ customerSuppliedEncryptionEnforcementConfig,
+ isUnreachable);
}
@Override
@@ -3985,7 +4017,8 @@ public boolean equals(Object o) {
that.customerManagedEncryptionEnforcementConfig)
&& Objects.equals(
customerSuppliedEncryptionEnforcementConfig,
- that.customerSuppliedEncryptionEnforcementConfig);
+ that.customerSuppliedEncryptionEnforcementConfig)
+ && Objects.equals(isUnreachable, that.isUnreachable);
}
@Override
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ByteSizeConstants.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ByteSizeConstants.java
index 463df327f5..c7110c1b12 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/ByteSizeConstants.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ByteSizeConstants.java
@@ -37,6 +37,7 @@ final class ByteSizeConstants {
static final long _256KiBL = 262144L;
static final long _512KiBL = 524288L;
static final long _768KiBL = 786432L;
+ static final long _1MiBL = 1048576L;
private ByteSizeConstants() {}
}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java
index 53a14ca264..6d163f1d81 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultBlobWriteSessionConfig.java
@@ -193,7 +193,12 @@ public WritableByteChannelSession, BlobInfo> writeSession(
ApiFuture startAsync =
ApiFutures.immediateFuture(
JsonResumableWrite.of(
- encode, optionsMap, uploadIdSupplier.get(), 0L));
+ encode,
+ optionsMap,
+ uploadIdSupplier.get(),
+ 0L,
+ opts.getHasher(),
+ opts.getHasher().initialValue()));
return ResumableMedia.http()
.write()
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
index d79b30e1fc..b536340e9c 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
@@ -454,6 +454,7 @@ public Page list(BucketListOption... options) {
Opts opts = Opts.unwrap(options).prepend(defaultOpts).prepend(ALL_BUCKET_FIELDS);
GrpcCallContext grpcCallContext =
opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
+
ListBucketsRequest request =
defaultProjectId
.get()
@@ -461,19 +462,29 @@ public Page list(BucketListOption... options) {
.andThen(opts.listBucketsRequest())
.apply(ListBucketsRequest.newBuilder())
.build();
- try {
- GrpcCallContext merge = Utils.merge(grpcCallContext, Retrying.newCallContext());
- return retrier.run(
- retryAlgorithmManager.getFor(request),
- () -> storageClient.listBucketsPagedCallable().call(request, merge),
- resp ->
- new TransformingPageDecorator<>(
- resp.getPage(),
- syntaxDecoders.bucket.andThen(opts.clearBucketFields()),
- retrier,
- retryAlgorithmManager.getFor(request)));
- } catch (Exception e) {
- throw StorageException.coalesce(e);
+
+ if (!request.getReturnPartialSuccess()) {
+ try {
+ GrpcCallContext merge = Utils.merge(grpcCallContext, Retrying.newCallContext());
+ return retrier.run(
+ retryAlgorithmManager.getFor(request),
+ () -> storageClient.listBucketsPagedCallable().call(request, merge),
+ resp ->
+ new TransformingPageDecorator<>(
+ resp.getPage(),
+ syntaxDecoders.bucket.andThen(opts.clearBucketFields()),
+ retrier,
+ retryAlgorithmManager.getFor(request)));
+ } catch (Exception e) {
+ throw StorageException.coalesce(e);
+ }
+ } else {
+ try {
+ com.google.storage.v2.ListBucketsResponse response = listBuckets(grpcCallContext, request);
+ return new ListBucketsWithPartialSuccessPage(grpcCallContext, request, response, opts);
+ } catch (Exception e) {
+ throw StorageException.coalesce(e);
+ }
}
}
@@ -1619,6 +1630,78 @@ public Iterable getValues() {
}
}
+ private final class ListBucketsWithPartialSuccessPage implements Page {
+
+ private final GrpcCallContext ctx;
+ private final ListBucketsRequest req;
+ private final com.google.storage.v2.ListBucketsResponse resp;
+ private final Opts opts;
+
+ private ListBucketsWithPartialSuccessPage(
+ GrpcCallContext ctx,
+ ListBucketsRequest req,
+ com.google.storage.v2.ListBucketsResponse resp,
+ Opts opts) {
+ this.ctx = ctx;
+ this.req = req;
+ this.resp = resp;
+ this.opts = opts;
+ }
+
+ @Override
+ public boolean hasNextPage() {
+ return !resp.getNextPageToken().isEmpty();
+ }
+
+ @Override
+ public String getNextPageToken() {
+ return resp.getNextPageToken();
+ }
+
+ @Override
+ public Page getNextPage() {
+ if (!hasNextPage()) {
+ return null;
+ }
+ ListBucketsRequest nextPageReq =
+ req.toBuilder().setPageToken(resp.getNextPageToken()).build();
+ try {
+ com.google.storage.v2.ListBucketsResponse nextPageResp = listBuckets(ctx, nextPageReq);
+ return new ListBucketsWithPartialSuccessPage(ctx, nextPageReq, nextPageResp, opts);
+ } catch (Exception e) {
+ throw StorageException.coalesce(e);
+ }
+ }
+
+ @Override
+ public Iterable getValues() {
+ Decoder bucketDecoder =
+ syntaxDecoders.bucket.andThen(opts.clearBucketFields());
+ Stream reachable = resp.getBucketsList().stream().map(bucketDecoder::decode);
+ Stream unreachable =
+ resp.getUnreachableList().stream()
+ .map(
+ name -> {
+ String decoded = bucketNameCodec.decode(name);
+ return BucketInfo.newBuilder(decoded)
+ .setIsUnreachable(true)
+ .build()
+ .asBucket(GrpcStorageImpl.this);
+ });
+ return Streams.concat(reachable, unreachable).collect(ImmutableList.toImmutableList());
+ }
+
+ @Override
+ public Iterable iterateAll() {
+ Page curr = this;
+ return () ->
+ streamIterate(curr, p -> p != null && p.hasNextPage(), Page::getNextPage)
+ .filter(Objects::nonNull)
+ .flatMap(p -> StreamSupport.stream(p.getValues().spliterator(), false))
+ .iterator();
+ }
+ }
+
static final class TransformingPageDecorator<
RequestT,
ResponseT,
@@ -1858,6 +1941,15 @@ private SourceObject sourceObjectEncode(SourceBlob from) {
return to.build();
}
+ private com.google.storage.v2.ListBucketsResponse listBuckets(
+ GrpcCallContext grpcCallContext, ListBucketsRequest request) {
+ GrpcCallContext merge = Utils.merge(grpcCallContext, Retrying.newCallContext());
+ return retrier.run(
+ retryAlgorithmManager.getFor(request),
+ () -> storageClient.listBucketsCallable().call(request, merge),
+ Decoder.identity());
+ }
+
private com.google.storage.v2.Bucket getBucketWithDefaultAcls(String bucketName) {
Fields fields =
UnifiedOpts.fields(
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Hasher.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Hasher.java
index 47a7b029e0..c1b506de2f 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Hasher.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Hasher.java
@@ -73,8 +73,8 @@ default Crc32cLengthKnown hash(Supplier b) {
void validateUnchecked(Crc32cValue> expected, ByteString byteString)
throws UncheckedChecksumMismatchException;
- @Nullable Crc32cLengthKnown nullSafeConcat(
- @Nullable Crc32cLengthKnown r1, @NonNull Crc32cLengthKnown r2);
+ @Nullable > C nullSafeConcat(
+ @Nullable C r1, @Nullable Crc32cLengthKnown r2);
/**
* The initial value to use for this hasher.
@@ -123,8 +123,8 @@ public void validate(Crc32cValue> expected, ByteString b) {}
public void validateUnchecked(Crc32cValue> expected, ByteString byteString) {}
@Override
- public @Nullable Crc32cLengthKnown nullSafeConcat(
- @Nullable Crc32cLengthKnown r1, @NonNull Crc32cLengthKnown r2) {
+ public > @Nullable C nullSafeConcat(
+ @Nullable C r1, @Nullable Crc32cLengthKnown r2) {
return null;
}
@@ -189,14 +189,16 @@ public void validateUnchecked(Crc32cValue> expected, ByteString byteString)
}
}
+ @SuppressWarnings("unchecked")
@Override
- @Nullable
- public Crc32cLengthKnown nullSafeConcat(
- @Nullable Crc32cLengthKnown r1, @NonNull Crc32cLengthKnown r2) {
+ public > @Nullable C nullSafeConcat(
+ @Nullable C r1, @Nullable Crc32cLengthKnown r2) {
if (r1 == null) {
return null;
+ } else if (r2 == null) {
+ return r1;
} else {
- return r1.concat(r2);
+ return (C) r1.concat(r2);
}
}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpContentRange.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpContentRange.java
index a069c24950..43c68c02b9 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpContentRange.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpContentRange.java
@@ -210,7 +210,7 @@ public String getHeaderValue() {
@Override
public boolean endOffsetEquals(long e) {
- return false;
+ return e == Math.max(0, size - 1);
}
@Override
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpWritableByteChannelSessionBuilder.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpWritableByteChannelSessionBuilder.java
index a5474b5aee..ddb999e9b7 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpWritableByteChannelSessionBuilder.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpWritableByteChannelSessionBuilder.java
@@ -57,11 +57,13 @@ static final class ResumableUploadBuilder {
@NonNull private final HttpClientContext httpClientContext;
private RetrierWithAlg retrier;
private LongConsumer committedBytesCallback;
+ private Hasher hasher;
ResumableUploadBuilder(@NonNull HttpClientContext httpClientContext) {
this.httpClientContext = httpClientContext;
this.retrier = RetrierWithAlg.attemptOnce();
this.committedBytesCallback = l -> {};
+ this.hasher = Hasher.defaultHasher();
}
ResumableUploadBuilder setCommittedBytesCallback(@NonNull LongConsumer committedBytesCallback) {
@@ -75,6 +77,11 @@ ResumableUploadBuilder withRetryConfig(@NonNull RetrierWithAlg retrier) {
return this;
}
+ ResumableUploadBuilder setHasher(@NonNull Hasher hasher) {
+ this.hasher = requireNonNull(hasher, "hasher must be non null");
+ return this;
+ }
+
/**
* Do not apply any intermediate buffering. Any call to {@link
* java.nio.channels.WritableByteChannel#write(ByteBuffer)} will be segmented as is and sent to
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonConversions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonConversions.java
index 08a908b12f..48a8558977 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonConversions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonConversions.java
@@ -18,6 +18,7 @@
import static com.google.cloud.storage.Storage.BucketField.IP_FILTER;
import static com.google.cloud.storage.Storage.BucketField.SOFT_DELETE_POLICY;
+import static com.google.cloud.storage.Utils.bucketNameCodec;
import static com.google.cloud.storage.Utils.dateTimeCodec;
import static com.google.cloud.storage.Utils.durationSecondsCodec;
import static com.google.cloud.storage.Utils.ifNonNull;
@@ -609,7 +610,7 @@ private Bucket bucketInfoEncode(BucketInfo from) {
@SuppressWarnings("deprecation")
private BucketInfo bucketInfoDecode(com.google.api.services.storage.model.Bucket from) {
- BucketInfo.Builder to = new BucketInfo.BuilderImpl(from.getName());
+ BucketInfo.Builder to = new BucketInfo.BuilderImpl(bucketNameCodec.decode(from.getName()));
ifNonNull(from.getProjectNumber(), to::setProject);
ifNonNull(from.getAcl(), toListOf(bucketAcl()::decode), to::setAcl);
ifNonNull(from.getCors(), toListOf(cors()::decode), to::setCors);
@@ -674,6 +675,9 @@ private BucketInfo bucketInfoDecode(com.google.api.services.storage.model.Bucket
ifNonNull(from.getObjectRetention(), this::objectRetentionDecode, to::setObjectRetention);
ifNonNull(from.getSoftDeletePolicy(), this::softDeletePolicyDecode, to::setSoftDeletePolicy);
ifNonNull(from.getIpFilter(), ipFilterCodec::decode, to::setIpFilter);
+ if (from.containsKey("isUnreachable")) {
+ to.setIsUnreachable(Boolean.TRUE);
+ }
return to.build();
}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSession.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSession.java
index d4347787e0..7003bd5d6c 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSession.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSession.java
@@ -18,10 +18,14 @@
import com.google.api.services.storage.model.StorageObject;
import com.google.cloud.storage.Conversions.Decoder;
+import com.google.cloud.storage.Crc32cValue.Crc32cLengthKnown;
+import com.google.cloud.storage.HttpContentRange.HasRange;
import com.google.cloud.storage.Retrying.RetrierWithAlg;
import com.google.cloud.storage.spi.v1.HttpRpcContext;
import com.google.cloud.storage.spi.v1.HttpStorageRpc;
import io.opencensus.trace.EndSpanOptions;
+import java.nio.ByteBuffer;
+import java.nio.channels.GatheringByteChannel;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import org.checkerframework.checker.nullness.qual.Nullable;
@@ -54,28 +58,119 @@ final class JsonResumableSession {
ResumableOperationResult<@Nullable StorageObject> put(
RewindableContent content, HttpContentRange contentRange) {
+ Crc32cValue> crc32cSoFar = resumableWrite.getCumulativeCrc32c();
+ @Nullable Crc32cValue> nextCumulativeCrc32c =
+ resumableWrite.getHasher().nullSafeConcat(crc32cSoFar, content.getCrc32c());
+ @Nullable Crc32cValue> finalChecksum =
+ contentRange.isFinalizing() ? nextCumulativeCrc32c : null;
JsonResumableSessionPutTask task =
- new JsonResumableSessionPutTask(context, resumableWrite, content, contentRange);
+ new JsonResumableSessionPutTask(
+ context, resumableWrite, content, contentRange, finalChecksum);
HttpRpcContext httpRpcContext = HttpRpcContext.getInstance();
try {
httpRpcContext.newInvocationId();
AtomicBoolean dirty = new AtomicBoolean(false);
- return retrier.run(
- () -> {
- if (dirty.getAndSet(true)) {
- ResumableOperationResult<@Nullable StorageObject> query = query();
- long persistedSize = query.getPersistedSize();
- if (contentRange.endOffsetEquals(persistedSize) || query.getObject() != null) {
- return query;
- } else {
- task.rewindTo(persistedSize);
- }
- }
- return task.call();
- },
- Decoder.identity());
+ ResumableOperationResult<@Nullable StorageObject> result =
+ retrier.run(
+ () -> {
+ if (dirty.getAndSet(true)) {
+ ResumableOperationResult<@Nullable StorageObject> query = query();
+ long persistedSize = query.getPersistedSize();
+ if (contentRange.endOffsetEquals(persistedSize) || query.getObject() != null) {
+ return query;
+ } else {
+ task.rewindTo(persistedSize);
+ }
+ }
+ return task.call();
+ },
+ Decoder.identity());
+
+ if (nextCumulativeCrc32c != null) {
+ long persistedSize = result.getPersistedSize();
+ if (contentRange.endOffsetEquals(persistedSize) || result.getObject() != null) {
+ resumableWrite.setCumulativeCrc32c(nextCumulativeCrc32c);
+ } else if (contentRange instanceof HasRange) {
+ ByteRangeSpec range = ((HasRange>) contentRange).range();
+ content.rewindTo(0);
+ long serverConsumedBytes = persistedSize - range.beginOffset();
+ try (HashingGatheringByteChannel hashingChannel =
+ new HashingGatheringByteChannel(serverConsumedBytes)) {
+ StorageException.wrapIOException(() -> content.writeTo(hashingChannel));
+ resumableWrite.setCumulativeCrc32c(
+ resumableWrite.getHasher().nullSafeConcat(crc32cSoFar, hashingChannel.cumulative));
+ }
+ } else {
+ throw new StorageException(
+ 0,
+ String.format(
+ Locale.US,
+ "Result persistedSize (%d) did not match expected end of contentRange (%s) and"
+ + " contentRange does not have range to allow automatic recovery",
+ persistedSize,
+ contentRange));
+ }
+ }
+ return result;
} finally {
httpRpcContext.clearInvocationId();
}
}
+
+ private static final class HashingGatheringByteChannel implements GatheringByteChannel {
+ private final long maxBytesToConsume;
+
+ private Crc32cLengthKnown cumulative;
+
+ private HashingGatheringByteChannel(long maxBytesToConsume) {
+ this.maxBytesToConsume = maxBytesToConsume;
+ this.cumulative = Crc32cValue.zero();
+ }
+
+ @Override
+ public int write(ByteBuffer src) {
+ return Math.toIntExact(write(new ByteBuffer[] {src}, 0, 1));
+ }
+
+ @Override
+ public long write(ByteBuffer[] srcs) {
+ return write(srcs, 0, srcs.length);
+ }
+
+ @Override
+ public long write(ByteBuffer[] srcs, int offset, int length) {
+ Crc32cLengthKnown cum = Crc32cValue.zero();
+ for (int i = offset; i < length; i++) {
+ long toConsume = maxBytesToConsume - cum.getLength();
+ if (toConsume <= 0) {
+ if (cum.getLength() == 0) {
+ return -1;
+ } else {
+ break;
+ }
+ }
+
+ ByteBuffer buf = srcs[i];
+ if (buf.remaining() <= toConsume) {
+ cum = cum.concat(Hasher.enabled().hash(buf));
+ } else {
+ ByteBuffer slice = buf.slice();
+ int limit = Math.toIntExact(toConsume);
+ slice.limit(limit);
+ cum = cum.concat(Hasher.enabled().hash(slice));
+ buf.position(buf.position() + limit);
+ }
+ }
+ cumulative = cumulative.concat(cum);
+ return cum.getLength();
+ }
+
+ @Override
+ public boolean isOpen() {
+ return true;
+ }
+
+ @Override
+ public void close() {}
+ }
}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSessionPutTask.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSessionPutTask.java
index 92de549bd8..81018d5f40 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSessionPutTask.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableSessionPutTask.java
@@ -43,6 +43,7 @@ final class JsonResumableSessionPutTask
private final JsonResumableWrite jsonResumableWrite;
private final RewindableContent content;
private final HttpContentRange originalContentRange;
+ private final @Nullable Crc32cValue> cumulativeCrc32c;
private HttpContentRange contentRange;
@@ -52,11 +53,22 @@ final class JsonResumableSessionPutTask
JsonResumableWrite jsonResumableWrite,
RewindableContent content,
HttpContentRange originalContentRange) {
+ this(httpClientContext, jsonResumableWrite, content, originalContentRange, null);
+ }
+
+ @VisibleForTesting
+ JsonResumableSessionPutTask(
+ HttpClientContext httpClientContext,
+ JsonResumableWrite jsonResumableWrite,
+ RewindableContent content,
+ HttpContentRange originalContentRange,
+ @Nullable Crc32cValue> cumulativeCrc32c) {
this.context = httpClientContext;
this.jsonResumableWrite = jsonResumableWrite;
this.content = content;
this.originalContentRange = originalContentRange;
this.contentRange = originalContentRange;
+ this.cumulativeCrc32c = cumulativeCrc32c;
}
public void rewindTo(long offset) {
@@ -101,6 +113,9 @@ public void rewindTo(long offset) {
for (Entry e : jsonResumableWrite.getExtraHeaders().entrySet()) {
headers.set(e.getKey(), e.getValue());
}
+ if (cumulativeCrc32c != null) {
+ headers.set("x-goog-hash", "crc32c=" + Utils.crc32cCodec.encode(cumulativeCrc32c.getValue()));
+ }
HttpResponse response = null;
try {
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableWrite.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableWrite.java
index 41bbec72ec..b2347c47f3 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableWrite.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/JsonResumableWrite.java
@@ -31,6 +31,7 @@
import java.io.StringReader;
import java.util.Map;
import java.util.Objects;
+import org.checkerframework.checker.lock.qual.GuardedBy;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.NonNull;
@@ -39,6 +40,8 @@ final class JsonResumableWrite implements Serializable {
private static final Gson gson = new Gson();
@MonotonicNonNull private transient StorageObject object;
+ @MonotonicNonNull private transient Hasher hasher;
+ @MonotonicNonNull private transient Crc32cValue> cumulativeCrc32c;
@MonotonicNonNull private final Map options;
@MonotonicNonNull private final String signedUrl;
@@ -48,13 +51,20 @@ final class JsonResumableWrite implements Serializable {
private volatile String objectJson;
+ @GuardedBy("objectJson")
+ private String base64CumulativeCrc32c;
+
private JsonResumableWrite(
StorageObject object,
+ @MonotonicNonNull Hasher hasher,
+ @MonotonicNonNull Crc32cValue> cumulativeCrc32c,
Map options,
String signedUrl,
@NonNull String uploadId,
long beginOffset) {
this.object = object;
+ this.hasher = hasher;
+ this.cumulativeCrc32c = cumulativeCrc32c;
this.options = options;
this.signedUrl = signedUrl;
this.uploadId = uploadId;
@@ -85,7 +95,20 @@ public JsonResumableWrite withBeginOffset(long newBeginOffset) {
"New beginOffset must be >= existing beginOffset (%s >= %s)",
newBeginOffset,
beginOffset);
- return new JsonResumableWrite(object, options, signedUrl, uploadId, newBeginOffset);
+ return new JsonResumableWrite(
+ object, hasher, cumulativeCrc32c, options, signedUrl, uploadId, newBeginOffset);
+ }
+
+ public @MonotonicNonNull Hasher getHasher() {
+ return hasher;
+ }
+
+ public @MonotonicNonNull Crc32cValue> getCumulativeCrc32c() {
+ return cumulativeCrc32c;
+ }
+
+ public void setCumulativeCrc32c(Crc32cValue> cumulativeCrc32c) {
+ this.cumulativeCrc32c = cumulativeCrc32c;
}
@Override
@@ -99,6 +122,8 @@ public boolean equals(Object o) {
JsonResumableWrite that = (JsonResumableWrite) o;
return beginOffset == that.beginOffset
&& Objects.equals(object, that.object)
+ && Objects.equals(hasher, that.hasher)
+ && cumulativeCrc32c.eqValue(that.cumulativeCrc32c)
&& Objects.equals(options, that.options)
&& Objects.equals(signedUrl, that.signedUrl)
&& Objects.equals(uploadId, that.uploadId);
@@ -106,13 +131,16 @@ public boolean equals(Object o) {
@Override
public int hashCode() {
- return Objects.hash(object, options, signedUrl, uploadId, beginOffset);
+ return Objects.hash(
+ object, hasher, cumulativeCrc32c.getValue(), options, signedUrl, uploadId, beginOffset);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("object", object)
+ .add("hasher", hasher)
+ .add("cumulativeCrc32c", cumulativeCrc32c)
.add("options", options)
.add("signedUrl", signedUrl)
.add("uploadId", uploadId)
@@ -125,6 +153,7 @@ private String getObjectJson() {
synchronized (this) {
if (objectJson == null) {
objectJson = gson.toJson(object);
+ base64CumulativeCrc32c = Utils.crc32cCodec.encode(cumulativeCrc32c.getValue());
}
}
}
@@ -140,14 +169,38 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE
in.defaultReadObject();
JsonReader jsonReader = gson.newJsonReader(new StringReader(this.objectJson));
this.object = gson.fromJson(jsonReader, StorageObject.class);
+ if (base64CumulativeCrc32c != null) {
+ Integer decode = Utils.crc32cCodec.decode(base64CumulativeCrc32c);
+ if (decode == 0) {
+ this.cumulativeCrc32c = Crc32cValue.zero();
+ } else {
+ this.cumulativeCrc32c = Crc32cValue.of(decode);
+ }
+ this.hasher = Hasher.enabled();
+ }
}
static JsonResumableWrite of(
StorageObject req, Map options, String uploadId, long beginOffset) {
- return new JsonResumableWrite(req, options, null, uploadId, beginOffset);
+ return of(req, options, uploadId, beginOffset, Hasher.noop(), null);
+ }
+
+ static JsonResumableWrite of(
+ StorageObject req,
+ Map options,
+ String uploadId,
+ long beginOffset,
+ Hasher hasher,
+ Crc32cValue> initialValue) {
+ return new JsonResumableWrite(req, hasher, initialValue, options, null, uploadId, beginOffset);
}
static JsonResumableWrite of(String signedUrl, String uploadId, long beginOffset) {
- return new JsonResumableWrite(null, null, signedUrl, uploadId, beginOffset);
+ Hasher hasher = Hasher.noop();
+ if (beginOffset == 0) {
+ hasher = Hasher.defaultHasher();
+ }
+ return new JsonResumableWrite(
+ null, hasher, hasher.initialValue(), null, signedUrl, uploadId, beginOffset);
}
}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java
index 03ca85b9eb..4f80a7083d 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClient.java
@@ -24,6 +24,8 @@
import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadResponse;
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest;
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse;
+import com.google.cloud.storage.multipartupload.model.ListMultipartUploadsRequest;
+import com.google.cloud.storage.multipartupload.model.ListMultipartUploadsResponse;
import com.google.cloud.storage.multipartupload.model.ListPartsRequest;
import com.google.cloud.storage.multipartupload.model.ListPartsResponse;
import com.google.cloud.storage.multipartupload.model.UploadPartRequest;
@@ -99,6 +101,17 @@ public abstract CompleteMultipartUploadResponse completeMultipartUpload(
@BetaApi
public abstract UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requestBody);
+ /**
+ * Lists all multipart uploads in a bucket.
+ *
+ * @param request The request object containing the details for listing the multipart uploads.
+ * @return A {@link ListMultipartUploadsResponse} object containing the list of multipart uploads.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public abstract ListMultipartUploadsResponse listMultipartUploads(
+ ListMultipartUploadsRequest request);
+
/**
* Creates a new instance of {@link MultipartUploadClient}.
*
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java
index d67ed8ab5a..2aecba6232 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadClientImpl.java
@@ -23,6 +23,8 @@
import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadResponse;
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest;
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse;
+import com.google.cloud.storage.multipartupload.model.ListMultipartUploadsRequest;
+import com.google.cloud.storage.multipartupload.model.ListMultipartUploadsResponse;
import com.google.cloud.storage.multipartupload.model.ListPartsRequest;
import com.google.cloud.storage.multipartupload.model.ListPartsResponse;
import com.google.cloud.storage.multipartupload.model.UploadPartRequest;
@@ -96,4 +98,12 @@ public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requ
},
Decoder.identity());
}
+
+ @Override
+ public ListMultipartUploadsResponse listMultipartUploads(ListMultipartUploadsRequest request) {
+ return retrier.run(
+ retryAlgorithmManager.idempotent(),
+ () -> httpRequestManager.sendListMultipartUploadsRequest(request),
+ Decoder.identity());
+ }
}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java
index 1c099ff1b5..15d48d66a4 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/MultipartUploadHttpRequestManager.java
@@ -15,8 +15,6 @@
*/
package com.google.cloud.storage;
-import static com.google.cloud.storage.Utils.ifNonNull;
-
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.google.api.client.http.ByteArrayContent;
import com.google.api.client.http.GenericUrl;
@@ -36,6 +34,8 @@
import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadResponse;
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest;
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse;
+import com.google.cloud.storage.multipartupload.model.ListMultipartUploadsRequest;
+import com.google.cloud.storage.multipartupload.model.ListMultipartUploadsResponse;
import com.google.cloud.storage.multipartupload.model.ListPartsRequest;
import com.google.cloud.storage.multipartupload.model.ListPartsResponse;
import com.google.cloud.storage.multipartupload.model.UploadPartRequest;
@@ -83,7 +83,7 @@ CreateMultipartUploadResponse sendCreateMultipartUploadRequest(
HttpRequest httpRequest =
requestFactory.buildPostRequest(
- new GenericUrl(createUri), new ByteArrayContent(request.getContentType(), new byte[0]));
+ new GenericUrl(createUri), new ByteArrayContent(null, new byte[0]));
httpRequest.getHeaders().putAll(headerProvider.getHeaders());
addHeadersForCreateMultipartUpload(request, httpRequest.getHeaders());
httpRequest.setParser(objectParser);
@@ -98,11 +98,11 @@ ListPartsResponse sendListPartsRequest(ListPartsRequest request) throws IOExcept
.put("bucket", request.bucket())
.put("key", request.key())
.put("uploadId", request.uploadId());
- if (request.getMaxParts() != null) {
- params.put("max-parts", request.getMaxParts());
+ if (request.maxParts() != null) {
+ params.put("max-parts", request.maxParts());
}
- if (request.getPartNumberMarker() != null) {
- params.put("part-number-marker", request.getPartNumberMarker());
+ if (request.partNumberMarker() != null) {
+ params.put("part-number-marker", request.partNumberMarker());
}
String listUri =
@@ -113,11 +113,53 @@ ListPartsResponse sendListPartsRequest(ListPartsRequest request) throws IOExcept
false);
HttpRequest httpRequest = requestFactory.buildGetRequest(new GenericUrl(listUri));
httpRequest.getHeaders().putAll(headerProvider.getHeaders());
+ if (request.userProject() != null) {
+ httpRequest.getHeaders().put("x-goog-user-project", request.userProject());
+ }
httpRequest.setParser(objectParser);
httpRequest.setThrowExceptionOnExecuteError(true);
return httpRequest.execute().parseAs(ListPartsResponse.class);
}
+ ListMultipartUploadsResponse sendListMultipartUploadsRequest(ListMultipartUploadsRequest request)
+ throws IOException {
+
+ ImmutableMap.Builder params =
+ ImmutableMap.builder().put("bucket", request.bucket());
+ if (request.delimiter() != null) {
+ params.put("delimiter", request.delimiter());
+ }
+ if (request.encodingType() != null) {
+ params.put("encoding-type", request.encodingType());
+ }
+ if (request.keyMarker() != null) {
+ params.put("key-marker", request.keyMarker());
+ }
+ if (request.maxUploads() != null) {
+ params.put("max-uploads", request.maxUploads());
+ }
+ if (request.prefix() != null) {
+ params.put("prefix", request.prefix());
+ }
+ if (request.uploadIdMarker() != null) {
+ params.put("upload-id-marker", request.uploadIdMarker());
+ }
+ String listUri =
+ UriTemplate.expand(
+ uri.toString()
+ + "{bucket}?uploads{&delimiter,encoding-type,key-marker,max-uploads,prefix,upload-id-marker}",
+ params.build(),
+ false);
+ HttpRequest httpRequest = requestFactory.buildGetRequest(new GenericUrl(listUri));
+ httpRequest.getHeaders().putAll(headerProvider.getHeaders());
+ if (request.userProject() != null) {
+ httpRequest.getHeaders().put("x-goog-user-project", request.userProject());
+ }
+ httpRequest.setParser(objectParser);
+ httpRequest.setThrowExceptionOnExecuteError(true);
+ return httpRequest.execute().parseAs(ListMultipartUploadsResponse.class);
+ }
+
AbortMultipartUploadResponse sendAbortMultipartUploadRequest(AbortMultipartUploadRequest request)
throws IOException {
@@ -131,6 +173,9 @@ AbortMultipartUploadResponse sendAbortMultipartUploadRequest(AbortMultipartUploa
HttpRequest httpRequest = requestFactory.buildDeleteRequest(new GenericUrl(abortUri));
httpRequest.getHeaders().putAll(headerProvider.getHeaders());
+ if (request.userProject() != null) {
+ httpRequest.getHeaders().put("x-goog-user-project", request.userProject());
+ }
httpRequest.setParser(objectParser);
httpRequest.setThrowExceptionOnExecuteError(true);
return httpRequest.execute().parseAs(AbortMultipartUploadResponse.class);
@@ -149,6 +194,9 @@ CompleteMultipartUploadResponse sendCompleteMultipartUploadRequest(
requestFactory.buildPostRequest(
new GenericUrl(completeUri), new ByteArrayContent("application/xml", bytes));
httpRequest.getHeaders().putAll(headerProvider.getHeaders());
+ if (request.userProject() != null) {
+ httpRequest.getHeaders().put("x-goog-user-project", request.userProject());
+ }
@Nullable Crc32cLengthKnown crc32cValue = Hasher.defaultHasher().hash(ByteBuffer.wrap(bytes));
addChecksumHeader(crc32cValue, httpRequest.getHeaders());
httpRequest.setParser(objectParser);
@@ -174,8 +222,11 @@ UploadPartResponse sendUploadPartRequest(
HttpRequest httpRequest =
requestFactory.buildPutRequest(new GenericUrl(uploadUri), rewindableContent);
httpRequest.getHeaders().putAll(headerProvider.getHeaders());
- if (request.getCrc32c() != null) {
- addChecksumHeader(request.getCrc32c(), httpRequest.getHeaders());
+ if (request.userProject() != null) {
+ httpRequest.getHeaders().put("x-goog-user-project", request.userProject());
+ }
+ if (request.crc32c() != null) {
+ addChecksumHeader(request.crc32c(), httpRequest.getHeaders());
} else {
addChecksumHeader(rewindableContent.getCrc32c(), httpRequest.getHeaders());
}
@@ -198,12 +249,15 @@ static MultipartUploadHttpRequestManager createFrom(HttpStorageOptions options)
options.getLibraryVersion(),
formatName(StandardSystemProperty.OS_NAME.value()),
formatSemver(StandardSystemProperty.OS_VERSION.value())));
- ifNonNull(options.getProjectId(), pid -> stableHeaders.put("x-goog-user-project", pid));
return new MultipartUploadHttpRequestManager(
storage.getRequestFactory(),
new XmlObjectParser(new XmlMapper()),
options.getMergedHeaderProvider(FixedHeaderProvider.create(stableHeaders.build())),
- URI.create(options.getHost()));
+ URI.create(ensureTrailingSlash(options.getHost())));
+ }
+
+ private static String ensureTrailingSlash(String host) {
+ return host.endsWith("/") ? host : host + "/";
}
private void addChecksumHeader(@Nullable Crc32cLengthKnown crc32c, HttpHeaders headers) {
@@ -220,51 +274,51 @@ private void addChecksumHeader(@Nullable String crc32c, HttpHeaders headers) {
private void addHeadersForCreateMultipartUpload(
CreateMultipartUploadRequest request, HttpHeaders headers) {
- if (request.getCannedAcl() != null) {
- headers.put("x-goog-acl", request.getCannedAcl().getXmlEntry());
+ if (request.cannedAcl() != null) {
+ headers.put("x-goog-acl", request.cannedAcl().getXmlEntry());
}
- if (request.getMetadata() != null) {
- for (Map.Entry entry : request.getMetadata().entrySet()) {
+ if (request.metadata() != null) {
+ for (Map.Entry entry : request.metadata().entrySet()) {
if (entry.getKey() != null || entry.getValue() != null) {
headers.put("x-goog-meta-" + urlEncode(entry.getKey()), urlEncode(entry.getValue()));
}
}
}
- if (request.getContentType() != null) {
- headers.put("Content-Type", request.getContentType());
+ if (request.contentType() != null) {
+ headers.put("Content-Type", request.contentType());
}
- if (request.getContentDisposition() != null) {
- headers.put("Content-Disposition", request.getContentDisposition());
+ if (request.contentDisposition() != null) {
+ headers.put("Content-Disposition", request.contentDisposition());
}
- if (request.getContentEncoding() != null) {
- headers.put("Content-Encoding", request.getContentEncoding());
+ if (request.contentEncoding() != null) {
+ headers.put("Content-Encoding", request.contentEncoding());
}
- if (request.getContentLanguage() != null) {
- headers.put("Content-Language", request.getContentLanguage());
+ if (request.contentLanguage() != null) {
+ headers.put("Content-Language", request.contentLanguage());
}
- if (request.getCacheControl() != null) {
- headers.put("Cache-Control", request.getCacheControl());
+ if (request.cacheControl() != null) {
+ headers.put("Cache-Control", request.cacheControl());
}
- if (request.getStorageClass() != null) {
- headers.put("x-goog-storage-class", request.getStorageClass().toString());
+ if (request.storageClass() != null) {
+ headers.put("x-goog-storage-class", request.storageClass().toString());
}
- if (request.getKmsKeyName() != null && !request.getKmsKeyName().isEmpty()) {
- headers.put("x-goog-encryption-kms-key-name", request.getKmsKeyName());
+ if (request.kmsKeyName() != null && !request.kmsKeyName().isEmpty()) {
+ headers.put("x-goog-encryption-kms-key-name", request.kmsKeyName());
}
- if (request.getObjectLockMode() != null) {
- headers.put("x-goog-object-lock-mode", request.getObjectLockMode().toString());
+ if (request.objectLockMode() != null) {
+ headers.put("x-goog-object-lock-mode", request.objectLockMode().toString());
}
- if (request.getObjectLockRetainUntilDate() != null) {
+ if (request.objectLockRetainUntilDate() != null) {
headers.put(
"x-goog-object-lock-retain-until-date",
- Utils.offsetDateTimeRfc3339Codec.encode(request.getObjectLockRetainUntilDate()));
+ Utils.offsetDateTimeRfc3339Codec.encode(request.objectLockRetainUntilDate()));
}
- if (request.getCustomTime() != null) {
+ if (request.customTime() != null) {
headers.put(
- "x-goog-custom-time", Utils.offsetDateTimeRfc3339Codec.encode(request.getCustomTime()));
+ "x-goog-custom-time", Utils.offsetDateTimeRfc3339Codec.encode(request.customTime()));
}
- if (request.getUserProject() != null) {
- headers.put("x-goog-user-project", request.getUserProject());
+ if (request.userProject() != null) {
+ headers.put("x-goog-user-project", request.userProject());
}
}
@@ -282,7 +336,7 @@ private static String urlEncode(String value) {
*/
private static String formatName(String name) {
// Only lowercase letters, digits, and "-" are allowed
- return name.toLowerCase().replaceAll("[^\\w\\d\\-]", "-");
+ return name.toLowerCase().replaceAll("[^\\w-]", "-");
}
private static String formatSemver(String version) {
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
index 23e94e09f5..12ac95dff7 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
@@ -2470,6 +2470,11 @@ public static BucketListOption pageSize(long pageSize) {
return new BucketListOption(UnifiedOpts.pageSize(pageSize));
}
+ @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+ public static BucketListOption returnPartialSuccess(boolean returnPartialSuccess) {
+ return new BucketListOption(UnifiedOpts.returnPartialSuccess(returnPartialSuccess));
+ }
+
/** Returns an option to specify the page token from which to start listing buckets. */
@TransportCompatibility({Transport.HTTP, Transport.GRPC})
public static BucketListOption pageToken(@NonNull String pageToken) {
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java
index d4d4217840..ebc4cbe5d7 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java
@@ -216,7 +216,13 @@ public Blob createFrom(BlobInfo blobInfo, Path path, int bufferSize, BlobWriteOp
optionsMap,
retrier.withAlg(retryAlgorithmManager.getForResumableUploadSessionCreate(optionsMap)));
JsonResumableWrite jsonResumableWrite =
- JsonResumableWrite.of(encode, optionsMap, uploadIdSupplier.get(), 0);
+ JsonResumableWrite.of(
+ encode,
+ optionsMap,
+ uploadIdSupplier.get(),
+ 0,
+ opts.getHasher(),
+ opts.getHasher().initialValue());
JsonResumableSession session =
ResumableSession.json(
@@ -762,7 +768,13 @@ public StorageWriteChannel writer(BlobInfo blobInfo, BlobWriteOption... options)
optionsMap,
retrier.withAlg(retryAlgorithmManager.getForResumableUploadSessionCreate(optionsMap)));
JsonResumableWrite jsonResumableWrite =
- JsonResumableWrite.of(encode, optionsMap, uploadIdSupplier.get(), 0);
+ JsonResumableWrite.of(
+ encode,
+ optionsMap,
+ uploadIdSupplier.get(),
+ 0,
+ opts.getHasher(),
+ opts.getHasher().initialValue());
return new BlobWriteChannelV2(BlobReadChannelContext.from(this), jsonResumableWrite);
}
@@ -1724,7 +1736,13 @@ public BlobInfo internalCreateFrom(Path path, BlobInfo info, Opts listObjects() {
}
}
+ static final class ReturnPartialSuccess extends RpcOptVal implements BucketListOpt {
+ private static final long serialVersionUID = -1370658416509499277L;
+
+ private ReturnPartialSuccess(boolean val) {
+ super(StorageRpc.Option.RETURN_PARTIAL_SUCCESS, val);
+ }
+
+ @Override
+ public Mapper listBuckets() {
+ return b -> b.setReturnPartialSuccess(val);
+ }
+ }
+
static final class PredefinedAcl extends RpcOptVal
implements BucketTargetOpt, ObjectTargetOpt {
private static final long serialVersionUID = -1743736785228368741L;
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/UploadFailureScenario.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/UploadFailureScenario.java
index a8312cc19b..5335ce19cf 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/UploadFailureScenario.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/UploadFailureScenario.java
@@ -93,6 +93,7 @@ enum UploadFailureScenario {
.or(matches("Range"))
.or(startsWith("X-Goog-Stored-"))
.or(matches("X-Goog-GCS-Idempotency-Token"))
+ .or(matches("X-Goog-Hash"))
.or(matches("X-Goog-request-params"))
.or(matches("X-GUploader-UploadID"));
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/AbortMultipartUploadRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/AbortMultipartUploadRequest.java
index 2da671ff7d..8dddf99d1d 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/AbortMultipartUploadRequest.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/AbortMultipartUploadRequest.java
@@ -28,11 +28,13 @@ public final class AbortMultipartUploadRequest {
private final String bucket;
private final String key;
private final String uploadId;
+ private final String userProject;
private AbortMultipartUploadRequest(Builder builder) {
this.bucket = builder.bucket;
this.key = builder.key;
this.uploadId = builder.uploadId;
+ this.userProject = builder.userProject;
}
/**
@@ -68,6 +70,19 @@ public String uploadId() {
return uploadId;
}
+ /**
+ * Returns the user-project.
+ *
+ * @return the user-project.
+ * @see x-goog-user-project
+ * @since 2.61 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String userProject() {
+ return userProject;
+ }
+
/**
* Returns a new builder for creating {@link AbortMultipartUploadRequest} instances.
*
@@ -89,6 +104,7 @@ public static class Builder {
private String bucket;
private String key;
private String uploadId;
+ private String userProject;
private Builder() {}
@@ -131,6 +147,21 @@ public Builder uploadId(String uploadId) {
return this;
}
+ /**
+ * Sets the user-project.
+ *
+ * @param userProject The user-project.
+ * @return This builder.
+ * @see x-goog-user-project
+ * @since 2.61 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public Builder userProject(String userProject) {
+ this.userProject = userProject;
+ return this;
+ }
+
/**
* Builds a new {@link AbortMultipartUploadRequest} instance.
*
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CompleteMultipartUploadRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CompleteMultipartUploadRequest.java
index 291a10d3af..4f7a6a4cb0 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CompleteMultipartUploadRequest.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CompleteMultipartUploadRequest.java
@@ -32,12 +32,14 @@ public final class CompleteMultipartUploadRequest {
private final String key;
private final String uploadId;
private final CompletedMultipartUpload multipartUpload;
+ private final String userProject;
private CompleteMultipartUploadRequest(Builder builder) {
this.bucket = builder.bucket;
this.key = builder.key;
this.uploadId = builder.uploadId;
this.multipartUpload = builder.multipartUpload;
+ this.userProject = builder.userProject;
}
/**
@@ -84,6 +86,19 @@ public CompletedMultipartUpload multipartUpload() {
return multipartUpload;
}
+ /**
+ * Returns the user-project.
+ *
+ * @return the user-project.
+ * @see x-goog-user-project
+ * @since 2.61 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String userProject() {
+ return userProject;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -96,12 +111,13 @@ public boolean equals(Object o) {
return Objects.equals(bucket, that.bucket)
&& Objects.equals(key, that.key)
&& Objects.equals(uploadId, that.uploadId)
- && Objects.equals(multipartUpload, that.multipartUpload);
+ && Objects.equals(multipartUpload, that.multipartUpload)
+ && Objects.equals(userProject, that.userProject);
}
@Override
public int hashCode() {
- return Objects.hash(bucket, key, uploadId, multipartUpload);
+ return Objects.hash(bucket, key, uploadId, multipartUpload, userProject);
}
@Override
@@ -111,6 +127,7 @@ public String toString() {
.add("key", key)
.add("uploadId", uploadId)
.add("completedMultipartUpload", multipartUpload)
+ .add("userProject", userProject)
.toString();
}
@@ -136,6 +153,7 @@ public static class Builder {
private String key;
private String uploadId;
private CompletedMultipartUpload multipartUpload;
+ private String userProject;
private Builder() {}
@@ -191,6 +209,21 @@ public Builder multipartUpload(CompletedMultipartUpload completedMultipartUpload
return this;
}
+ /**
+ * Sets the user-project.
+ *
+ * @param userProject The user-project.
+ * @return This builder.
+ * @see x-goog-user-project
+ * @since 2.61 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public Builder userProject(String userProject) {
+ this.userProject = userProject;
+ return this;
+ }
+
/**
* Builds the {@link CompleteMultipartUploadRequest} object.
*
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java
index ab314ee2a6..4b58c6ec55 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/CreateMultipartUploadRequest.java
@@ -96,7 +96,7 @@ public String key() {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public PredefinedAcl getCannedAcl() {
+ public PredefinedAcl cannedAcl() {
return cannedAcl;
}
@@ -107,7 +107,7 @@ public PredefinedAcl getCannedAcl() {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public String getContentType() {
+ public String contentType() {
return contentType;
}
@@ -118,7 +118,7 @@ public String getContentType() {
* @since 2.61.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public String getContentDisposition() {
+ public String contentDisposition() {
return contentDisposition;
}
@@ -129,7 +129,7 @@ public String getContentDisposition() {
* @since 2.61.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public String getContentEncoding() {
+ public String contentEncoding() {
return contentEncoding;
}
@@ -140,7 +140,7 @@ public String getContentEncoding() {
* @since 2.61.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public String getContentLanguage() {
+ public String contentLanguage() {
return contentLanguage;
}
@@ -152,7 +152,7 @@ public String getContentLanguage() {
* @since 2.61.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public String getCacheControl() {
+ public String cacheControl() {
return cacheControl;
}
@@ -163,7 +163,7 @@ public String getCacheControl() {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public Map getMetadata() {
+ public Map metadata() {
return metadata;
}
@@ -174,7 +174,7 @@ public Map getMetadata() {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public StorageClass getStorageClass() {
+ public StorageClass storageClass() {
return storageClass;
}
@@ -185,7 +185,7 @@ public StorageClass getStorageClass() {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public OffsetDateTime getCustomTime() {
+ public OffsetDateTime customTime() {
return customTime;
}
@@ -196,7 +196,7 @@ public OffsetDateTime getCustomTime() {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public String getKmsKeyName() {
+ public String kmsKeyName() {
return kmsKeyName;
}
@@ -207,7 +207,7 @@ public String getKmsKeyName() {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public ObjectLockMode getObjectLockMode() {
+ public ObjectLockMode objectLockMode() {
return objectLockMode;
}
@@ -218,7 +218,7 @@ public ObjectLockMode getObjectLockMode() {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public OffsetDateTime getObjectLockRetainUntilDate() {
+ public OffsetDateTime objectLockRetainUntilDate() {
return objectLockRetainUntilDate;
}
@@ -228,7 +228,7 @@ public OffsetDateTime getObjectLockRetainUntilDate() {
* @return The user project
* @since 2.61.0 This new api is in preview and is subject to breaking changes.
*/
- public String getUserProject() {
+ public String userProject() {
return userProject;
}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListMultipartUploadsRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListMultipartUploadsRequest.java
new file mode 100644
index 0000000000..17e1a924dd
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListMultipartUploadsRequest.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.storage.multipartupload.model;
+
+import com.google.api.core.BetaApi;
+import com.google.common.base.MoreObjects;
+import java.util.Objects;
+
+/**
+ * A request to list all multipart uploads in a bucket.
+ *
+ * @see Listing
+ * multipart uploads
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+@BetaApi
+public final class ListMultipartUploadsRequest {
+
+ private final String bucket;
+ private final String delimiter;
+ private final String encodingType;
+ private final String keyMarker;
+ private final Integer maxUploads;
+ private final String prefix;
+ private final String uploadIdMarker;
+ private final String userProject;
+
+ private ListMultipartUploadsRequest(
+ String bucket,
+ String delimiter,
+ String encodingType,
+ String keyMarker,
+ Integer maxUploads,
+ String prefix,
+ String uploadIdMarker,
+ String userProject) {
+ this.bucket = bucket;
+ this.delimiter = delimiter;
+ this.encodingType = encodingType;
+ this.keyMarker = keyMarker;
+ this.maxUploads = maxUploads;
+ this.prefix = prefix;
+ this.uploadIdMarker = uploadIdMarker;
+ this.userProject = userProject;
+ }
+
+ /**
+ * The bucket to list multipart uploads from.
+ *
+ * @return The bucket name.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String bucket() {
+ return bucket;
+ }
+
+ /**
+ * Character used to group keys.
+ *
+ * @return The delimiter.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String delimiter() {
+ return delimiter;
+ }
+
+ /**
+ * The encoding type used by Cloud Storage to encode object names in the response.
+ *
+ * @return The encoding type.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String encodingType() {
+ return encodingType;
+ }
+
+ /**
+ * Together with {@code upload-id-marker}, specifies the multipart upload after which listing
+ * should begin.
+ *
+ * @return The key marker.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String keyMarker() {
+ return keyMarker;
+ }
+
+ /**
+ * The maximum number of multipart uploads to return.
+ *
+ * @return The maximum number of uploads.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public Integer maxUploads() {
+ return maxUploads;
+ }
+
+ /**
+ * Filters results to multipart uploads whose keys begin with this prefix.
+ *
+ * @return The prefix.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String prefix() {
+ return prefix;
+ }
+
+ /**
+ * Together with {@code key-marker}, specifies the multipart upload after which listing should
+ * begin.
+ *
+ * @return The upload ID marker.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String uploadIdMarker() {
+ return uploadIdMarker;
+ }
+
+ /**
+ * Returns the user-project.
+ *
+ * @return the user-project.
+ * @see x-goog-user-project
+ * @since 2.61 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String userProject() {
+ return userProject;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ListMultipartUploadsRequest that = (ListMultipartUploadsRequest) o;
+ return Objects.equals(bucket, that.bucket)
+ && Objects.equals(delimiter, that.delimiter)
+ && Objects.equals(encodingType, that.encodingType)
+ && Objects.equals(keyMarker, that.keyMarker)
+ && Objects.equals(maxUploads, that.maxUploads)
+ && Objects.equals(prefix, that.prefix)
+ && Objects.equals(uploadIdMarker, that.uploadIdMarker)
+ && Objects.equals(userProject, that.userProject);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ bucket,
+ delimiter,
+ encodingType,
+ keyMarker,
+ maxUploads,
+ prefix,
+ uploadIdMarker,
+ userProject);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("bucket", bucket)
+ .add("delimiter", delimiter)
+ .add("encodingType", encodingType)
+ .add("keyMarker", keyMarker)
+ .add("maxUploads", maxUploads)
+ .add("prefix", prefix)
+ .add("uploadIdMarker", uploadIdMarker)
+ .add("userProject", userProject)
+ .toString();
+ }
+
+ /**
+ * Returns a new builder for this request.
+ *
+ * @return A new builder.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * A builder for {@link ListMultipartUploadsRequest}.
+ *
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public static final class Builder {
+ private String bucket;
+ private String delimiter;
+ private String encodingType;
+ private String keyMarker;
+ private Integer maxUploads;
+ private String prefix;
+ private String uploadIdMarker;
+ private String userProject;
+
+ private Builder() {}
+
+ /**
+ * Sets the bucket to list multipart uploads from.
+ *
+ * @param bucket The bucket name.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public Builder bucket(String bucket) {
+ this.bucket = bucket;
+ return this;
+ }
+
+ /**
+ * Sets the delimiter used to group keys.
+ *
+ * @param delimiter The delimiter.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public Builder delimiter(String delimiter) {
+ this.delimiter = delimiter;
+ return this;
+ }
+
+ /**
+ * Sets the encoding type used by Cloud Storage to encode object names in the response.
+ *
+ * @param encodingType The encoding type.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public Builder encodingType(String encodingType) {
+ this.encodingType = encodingType;
+ return this;
+ }
+
+ /**
+ * Sets the key marker.
+ *
+ * @param keyMarker The key marker.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public Builder keyMarker(String keyMarker) {
+ this.keyMarker = keyMarker;
+ return this;
+ }
+
+ /**
+ * Sets the maximum number of multipart uploads to return.
+ *
+ * @param maxUploads The maximum number of uploads.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public Builder maxUploads(Integer maxUploads) {
+ this.maxUploads = maxUploads;
+ return this;
+ }
+
+ /**
+ * Sets the prefix to filter results.
+ *
+ * @param prefix The prefix.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public Builder prefix(String prefix) {
+ this.prefix = prefix;
+ return this;
+ }
+
+ /**
+ * Sets the upload ID marker.
+ *
+ * @param uploadIdMarker The upload ID marker.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public Builder uploadIdMarker(String uploadIdMarker) {
+ this.uploadIdMarker = uploadIdMarker;
+ return this;
+ }
+
+ /**
+ * Sets the user-project.
+ *
+ * @param userProject The user-project.
+ * @return This builder.
+ * @see x-goog-user-project
+ * @since 2.61 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public Builder userProject(String userProject) {
+ this.userProject = userProject;
+ return this;
+ }
+
+ /**
+ * Builds the request.
+ *
+ * @return The built request.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public ListMultipartUploadsRequest build() {
+ return new ListMultipartUploadsRequest(
+ bucket,
+ delimiter,
+ encodingType,
+ keyMarker,
+ maxUploads,
+ prefix,
+ uploadIdMarker,
+ userProject);
+ }
+ }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListMultipartUploadsResponse.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListMultipartUploadsResponse.java
new file mode 100644
index 0000000000..d43184bc15
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListMultipartUploadsResponse.java
@@ -0,0 +1,525 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.storage.multipartupload.model;
+
+import com.fasterxml.jackson.annotation.JsonAlias;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import com.google.api.core.BetaApi;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A response from listing all multipart uploads in a bucket.
+ *
+ * @see Listing
+ * multipart uploads
+ * @since 2.60.1 This new api is in preview and is subject to breaking changes.
+ */
+@BetaApi
+public final class ListMultipartUploadsResponse {
+
+ @JacksonXmlElementWrapper(useWrapping = false)
+ @JacksonXmlProperty(localName = "Upload")
+ private List uploads;
+
+ @JacksonXmlProperty(localName = "Bucket")
+ private String bucket;
+
+ @JacksonXmlProperty(localName = "Delimiter")
+ private String delimiter;
+
+ @JacksonXmlProperty(localName = "EncodingType")
+ private String encodingType;
+
+ @JacksonXmlProperty(localName = "KeyMarker")
+ private String keyMarker;
+
+ @JacksonXmlProperty(localName = "UploadIdMarker")
+ private String uploadIdMarker;
+
+ @JacksonXmlProperty(localName = "NextKeyMarker")
+ private String nextKeyMarker;
+
+ @JacksonXmlProperty(localName = "NextUploadIdMarker")
+ private String nextUploadIdMarker;
+
+ @JacksonXmlProperty(localName = "MaxUploads")
+ private int maxUploads;
+
+ @JacksonXmlProperty(localName = "Prefix")
+ private String prefix;
+
+ @JsonAlias("truncated")
+ @JacksonXmlProperty(localName = "IsTruncated")
+ private boolean isTruncated;
+
+ @JacksonXmlElementWrapper(useWrapping = false)
+ @JacksonXmlProperty(localName = "CommonPrefixes")
+ private List commonPrefixes;
+
+ // Jackson requires a no-arg constructor
+ private ListMultipartUploadsResponse() {}
+
+ private ListMultipartUploadsResponse(
+ List uploads,
+ String bucket,
+ String delimiter,
+ String encodingType,
+ String keyMarker,
+ String uploadIdMarker,
+ String nextKeyMarker,
+ String nextUploadIdMarker,
+ int maxUploads,
+ String prefix,
+ boolean isTruncated,
+ List commonPrefixes) {
+ this.uploads = uploads;
+ this.bucket = bucket;
+ this.delimiter = delimiter;
+ this.encodingType = encodingType;
+ this.keyMarker = keyMarker;
+ this.uploadIdMarker = uploadIdMarker;
+ this.nextKeyMarker = nextKeyMarker;
+ this.nextUploadIdMarker = nextUploadIdMarker;
+ this.maxUploads = maxUploads;
+ this.prefix = prefix;
+ this.isTruncated = isTruncated;
+ if (commonPrefixes != null) {
+ this.commonPrefixes = new ArrayList<>();
+ for (String p : commonPrefixes) {
+ CommonPrefixHelper h = new CommonPrefixHelper();
+ h.prefix = p;
+ this.commonPrefixes.add(h);
+ }
+ }
+ }
+
+ /**
+ * The list of multipart uploads.
+ *
+ * @return The list of multipart uploads.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public ImmutableList uploads() {
+ return uploads == null ? ImmutableList.of() : ImmutableList.copyOf(uploads);
+ }
+
+ /**
+ * The bucket that contains the multipart uploads.
+ *
+ * @return The bucket name.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String bucket() {
+ return bucket;
+ }
+
+ /**
+ * The delimiter applied to the request.
+ *
+ * @return The delimiter applied to the request.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String delimiter() {
+ return delimiter;
+ }
+
+ /**
+ * The encoding type used by Cloud Storage to encode object names in the response.
+ *
+ * @return The encoding type.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String encodingType() {
+ return encodingType;
+ }
+
+ /**
+ * The key at or after which the listing began.
+ *
+ * @return The key marker.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String keyMarker() {
+ return keyMarker;
+ }
+
+ /**
+ * The upload ID at or after which the listing began.
+ *
+ * @return The upload ID marker.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String uploadIdMarker() {
+ return uploadIdMarker;
+ }
+
+ /**
+ * The key after which listing should begin.
+ *
+ * @return The key after which listing should begin.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String nextKeyMarker() {
+ return nextKeyMarker;
+ }
+
+ /**
+ * The upload ID after which listing should begin.
+ *
+ * @return The upload ID after which listing should begin.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String nextUploadIdMarker() {
+ return nextUploadIdMarker;
+ }
+
+ /**
+ * The maximum number of uploads to return.
+ *
+ * @return The maximum number of uploads.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public int maxUploads() {
+ return maxUploads;
+ }
+
+ /**
+ * The prefix applied to the request.
+ *
+ * @return The prefix applied to the request.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String prefix() {
+ return prefix;
+ }
+
+ /**
+ * A flag indicating whether or not the returned results are truncated.
+ *
+ * @return A flag indicating whether or not the returned results are truncated.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public boolean truncated() {
+ return isTruncated;
+ }
+
+ /**
+ * If you specify a delimiter in the request, this element is returned.
+ *
+ * @return The common prefixes.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public ImmutableList commonPrefixes() {
+ if (commonPrefixes == null) {
+ return ImmutableList.of();
+ }
+ return commonPrefixes.stream().map(h -> h.prefix).collect(ImmutableList.toImmutableList());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ListMultipartUploadsResponse that = (ListMultipartUploadsResponse) o;
+ return isTruncated == that.isTruncated
+ && maxUploads == that.maxUploads
+ && Objects.equals(uploads(), that.uploads())
+ && Objects.equals(bucket, that.bucket)
+ && Objects.equals(delimiter, that.delimiter)
+ && Objects.equals(encodingType, that.encodingType)
+ && Objects.equals(keyMarker, that.keyMarker)
+ && Objects.equals(uploadIdMarker, that.uploadIdMarker)
+ && Objects.equals(nextKeyMarker, that.nextKeyMarker)
+ && Objects.equals(nextUploadIdMarker, that.nextUploadIdMarker)
+ && Objects.equals(prefix, that.prefix)
+ && Objects.equals(commonPrefixes(), that.commonPrefixes());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ uploads(),
+ bucket,
+ delimiter,
+ encodingType,
+ keyMarker,
+ uploadIdMarker,
+ nextKeyMarker,
+ nextUploadIdMarker,
+ maxUploads,
+ prefix,
+ isTruncated,
+ commonPrefixes());
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("uploads", uploads())
+ .add("bucket", bucket)
+ .add("delimiter", delimiter)
+ .add("encodingType", encodingType)
+ .add("keyMarker", keyMarker)
+ .add("uploadIdMarker", uploadIdMarker)
+ .add("nextKeyMarker", nextKeyMarker)
+ .add("nextUploadIdMarker", nextUploadIdMarker)
+ .add("maxUploads", maxUploads)
+ .add("prefix", prefix)
+ .add("isTruncated", isTruncated)
+ .add("commonPrefixes", commonPrefixes())
+ .toString();
+ }
+
+ /**
+ * Returns a new builder for this response.
+ *
+ * @return A new builder.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ static class CommonPrefixHelper {
+ @JacksonXmlProperty(localName = "Prefix")
+ public String prefix;
+ }
+
+ /**
+ * A builder for {@link ListMultipartUploadsResponse}.
+ *
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public static final class Builder {
+ private ImmutableList uploads;
+ private String bucket;
+ private String delimiter;
+ private String encodingType;
+ private String keyMarker;
+ private String uploadIdMarker;
+ private String nextKeyMarker;
+ private String nextUploadIdMarker;
+ private int maxUploads;
+ private String prefix;
+ private boolean isTruncated;
+ private ImmutableList commonPrefixes;
+
+ private Builder() {}
+
+ /**
+ * Sets the list of multipart uploads.
+ *
+ * @param uploads The list of multipart uploads.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview.
+ */
+ @BetaApi
+ public Builder uploads(ImmutableList uploads) {
+ this.uploads = uploads;
+ return this;
+ }
+
+ /**
+ * Sets the bucket that contains the multipart uploads.
+ *
+ * @param bucket The bucket name.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview.
+ */
+ @BetaApi
+ public Builder bucket(String bucket) {
+ this.bucket = bucket;
+ return this;
+ }
+
+ /**
+ * Sets the delimiter applied to the request.
+ *
+ * @param delimiter The delimiter applied to the request.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview.
+ */
+ @BetaApi
+ public Builder delimiter(String delimiter) {
+ this.delimiter = delimiter;
+ return this;
+ }
+
+ /**
+ * Sets the encoding type used by Cloud Storage to encode object names in the response.
+ *
+ * @param encodingType The encoding type.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview.
+ */
+ @BetaApi
+ public Builder encodingType(String encodingType) {
+ this.encodingType = encodingType;
+ return this;
+ }
+
+ /**
+ * Sets the key at or after which the listing began.
+ *
+ * @param keyMarker The key marker.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview.
+ */
+ @BetaApi
+ public Builder keyMarker(String keyMarker) {
+ this.keyMarker = keyMarker;
+ return this;
+ }
+
+ /**
+ * Sets the upload ID at or after which the listing began.
+ *
+ * @param uploadIdMarker The upload ID marker.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview.
+ */
+ @BetaApi
+ public Builder uploadIdMarker(String uploadIdMarker) {
+ this.uploadIdMarker = uploadIdMarker;
+ return this;
+ }
+
+ /**
+ * Sets the key after which listing should begin.
+ *
+ * @param nextKeyMarker The key after which listing should begin.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview.
+ */
+ @BetaApi
+ public Builder nextKeyMarker(String nextKeyMarker) {
+ this.nextKeyMarker = nextKeyMarker;
+ return this;
+ }
+
+ /**
+ * Sets the upload ID after which listing should begin.
+ *
+ * @param nextUploadIdMarker The upload ID after which listing should begin.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview.
+ */
+ @BetaApi
+ public Builder nextUploadIdMarker(String nextUploadIdMarker) {
+ this.nextUploadIdMarker = nextUploadIdMarker;
+ return this;
+ }
+
+ /**
+ * Sets the maximum number of uploads to return.
+ *
+ * @param maxUploads The maximum number of uploads.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview.
+ */
+ @BetaApi
+ public Builder maxUploads(int maxUploads) {
+ this.maxUploads = maxUploads;
+ return this;
+ }
+
+ /**
+ * Sets the prefix applied to the request.
+ *
+ * @param prefix The prefix applied to the request.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview.
+ */
+ @BetaApi
+ public Builder prefix(String prefix) {
+ this.prefix = prefix;
+ return this;
+ }
+
+ /**
+ * Sets the flag indicating whether or not the returned results are truncated.
+ *
+ * @param isTruncated The flag indicating whether or not the returned results are truncated.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview.
+ */
+ @BetaApi
+ public Builder truncated(boolean isTruncated) {
+ this.isTruncated = isTruncated;
+ return this;
+ }
+
+ /**
+ * If you specify a delimiter in the request, this element is returned.
+ *
+ * @param commonPrefixes The common prefixes.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview.
+ */
+ @BetaApi
+ public Builder commonPrefixes(ImmutableList commonPrefixes) {
+ this.commonPrefixes = commonPrefixes;
+ return this;
+ }
+
+ /**
+ * Builds the response.
+ *
+ * @return The built response.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public ListMultipartUploadsResponse build() {
+ return new ListMultipartUploadsResponse(
+ uploads,
+ bucket,
+ delimiter,
+ encodingType,
+ keyMarker,
+ uploadIdMarker,
+ nextKeyMarker,
+ nextUploadIdMarker,
+ maxUploads,
+ prefix,
+ isTruncated,
+ commonPrefixes);
+ }
+ }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java
index 2062bdfeb2..871f1778ac 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsRequest.java
@@ -37,12 +37,15 @@ public final class ListPartsRequest {
private final Integer partNumberMarker;
+ private final String userProject;
+
private ListPartsRequest(Builder builder) {
this.bucket = builder.bucket;
this.key = builder.key;
this.uploadId = builder.uploadId;
this.maxParts = builder.maxParts;
this.partNumberMarker = builder.partNumberMarker;
+ this.userProject = builder.userProject;
}
/**
@@ -85,7 +88,7 @@ public String uploadId() {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public Integer getMaxParts() {
+ public Integer maxParts() {
return maxParts;
}
@@ -96,10 +99,23 @@ public Integer getMaxParts() {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public Integer getPartNumberMarker() {
+ public Integer partNumberMarker() {
return partNumberMarker;
}
+ /**
+ * Returns the user-project.
+ *
+ * @return the user-project.
+ * @see x-goog-user-project
+ * @since 2.61 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String userProject() {
+ return userProject;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -113,12 +129,13 @@ public boolean equals(Object o) {
&& Objects.equals(key, that.key)
&& Objects.equals(uploadId, that.uploadId)
&& Objects.equals(maxParts, that.maxParts)
- && Objects.equals(partNumberMarker, that.partNumberMarker);
+ && Objects.equals(partNumberMarker, that.partNumberMarker)
+ && Objects.equals(userProject, that.userProject);
}
@Override
public int hashCode() {
- return Objects.hash(bucket, key, uploadId, maxParts, partNumberMarker);
+ return Objects.hash(bucket, key, uploadId, maxParts, partNumberMarker, userProject);
}
@Override
@@ -129,6 +146,7 @@ public String toString() {
.add("uploadId", uploadId)
.add("maxParts", maxParts)
.add("partNumberMarker", partNumberMarker)
+ .add("userProject", userProject)
.toString();
}
@@ -155,6 +173,7 @@ public static class Builder {
private String uploadId;
private Integer maxParts;
private Integer partNumberMarker;
+ private String userProject;
private Builder() {}
@@ -223,6 +242,21 @@ public Builder partNumberMarker(Integer partNumberMarker) {
return this;
}
+ /**
+ * Sets the user-project.
+ *
+ * @param userProject The user-project.
+ * @return This builder.
+ * @see x-goog-user-project
+ * @since 2.61 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public Builder userProject(String userProject) {
+ this.userProject = userProject;
+ return this;
+ }
+
/**
* Builds a new {@link ListPartsRequest} object.
*
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java
index 0b8f82c5a0..b9625a4c50 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/ListPartsResponse.java
@@ -94,7 +94,7 @@ public static Builder builder() {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public String getBucket() {
+ public String bucket() {
return bucket;
}
@@ -105,7 +105,7 @@ public String getBucket() {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public String getKey() {
+ public String key() {
return key;
}
@@ -116,7 +116,7 @@ public String getKey() {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public String getUploadId() {
+ public String uploadId() {
return uploadId;
}
@@ -127,7 +127,7 @@ public String getUploadId() {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public int getPartNumberMarker() {
+ public int partNumberMarker() {
return partNumberMarker;
}
@@ -138,7 +138,7 @@ public int getPartNumberMarker() {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public int getNextPartNumberMarker() {
+ public int nextPartNumberMarker() {
return nextPartNumberMarker;
}
@@ -149,7 +149,7 @@ public int getNextPartNumberMarker() {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public int getMaxParts() {
+ public int maxParts() {
return maxParts;
}
@@ -160,7 +160,7 @@ public int getMaxParts() {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public boolean isTruncated() {
+ public boolean truncated() {
return isTruncated;
}
@@ -171,7 +171,7 @@ public boolean isTruncated() {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public StorageClass getStorageClass() {
+ public StorageClass storageClass() {
return storageClass;
}
@@ -182,7 +182,7 @@ public StorageClass getStorageClass() {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public List getParts() {
+ public List parts() {
return parts;
}
@@ -262,7 +262,7 @@ private Builder() {}
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public Builder setBucket(String bucket) {
+ public Builder bucket(String bucket) {
this.bucket = bucket;
return this;
}
@@ -275,7 +275,7 @@ public Builder setBucket(String bucket) {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public Builder setKey(String key) {
+ public Builder key(String key) {
this.key = key;
return this;
}
@@ -288,7 +288,7 @@ public Builder setKey(String key) {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public Builder setUploadId(String uploadId) {
+ public Builder uploadId(String uploadId) {
this.uploadId = uploadId;
return this;
}
@@ -301,7 +301,7 @@ public Builder setUploadId(String uploadId) {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public Builder setPartNumberMarker(int partNumberMarker) {
+ public Builder partNumberMarker(int partNumberMarker) {
this.partNumberMarker = partNumberMarker;
return this;
}
@@ -314,7 +314,7 @@ public Builder setPartNumberMarker(int partNumberMarker) {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public Builder setNextPartNumberMarker(int nextPartNumberMarker) {
+ public Builder nextPartNumberMarker(int nextPartNumberMarker) {
this.nextPartNumberMarker = nextPartNumberMarker;
return this;
}
@@ -327,7 +327,7 @@ public Builder setNextPartNumberMarker(int nextPartNumberMarker) {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public Builder setMaxParts(int maxParts) {
+ public Builder maxParts(int maxParts) {
this.maxParts = maxParts;
return this;
}
@@ -340,7 +340,7 @@ public Builder setMaxParts(int maxParts) {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public Builder setIsTruncated(boolean isTruncated) {
+ public Builder truncated(boolean isTruncated) {
this.isTruncated = isTruncated;
return this;
}
@@ -353,7 +353,7 @@ public Builder setIsTruncated(boolean isTruncated) {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public Builder setStorageClass(StorageClass storageClass) {
+ public Builder storageClass(StorageClass storageClass) {
this.storageClass = storageClass;
return this;
}
@@ -366,7 +366,7 @@ public Builder setStorageClass(StorageClass storageClass) {
* @since 2.60.0 This new api is in preview and is subject to breaking changes.
*/
@BetaApi
- public Builder setParts(List parts) {
+ public Builder parts(List parts) {
this.parts = parts;
return this;
}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/MultipartUpload.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/MultipartUpload.java
new file mode 100644
index 0000000000..26a44fb2f0
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/MultipartUpload.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2025 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.storage.multipartupload.model;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import com.google.api.core.BetaApi;
+import com.google.cloud.storage.StorageClass;
+import com.google.common.base.MoreObjects;
+import java.time.OffsetDateTime;
+import java.util.Objects;
+
+/**
+ * Represents a multipart upload that is in progress.
+ *
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+@BetaApi
+public final class MultipartUpload {
+
+ @JacksonXmlProperty(localName = "Key")
+ private String key;
+
+ @JacksonXmlProperty(localName = "UploadId")
+ private String uploadId;
+
+ @JacksonXmlProperty(localName = "StorageClass")
+ private StorageClass storageClass;
+
+ @JacksonXmlProperty(localName = "Initiated")
+ private OffsetDateTime initiated;
+
+ private MultipartUpload() {}
+
+ private MultipartUpload(
+ String key, String uploadId, StorageClass storageClass, OffsetDateTime initiated) {
+ this.key = key;
+ this.uploadId = uploadId;
+ this.storageClass = storageClass;
+ this.initiated = initiated;
+ }
+
+ /**
+ * The object name for which the multipart upload was initiated.
+ *
+ * @return The object name.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String key() {
+ return key;
+ }
+
+ /**
+ * The ID of the multipart upload.
+ *
+ * @return The upload ID.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String uploadId() {
+ return uploadId;
+ }
+
+ /**
+ * The storage class of the object.
+ *
+ * @return The storage class.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public StorageClass storageClass() {
+ return storageClass;
+ }
+
+ /**
+ * The date and time at which the multipart upload was initiated.
+ *
+ * @return The initiation date and time.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public OffsetDateTime initiated() {
+ return initiated;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ MultipartUpload that = (MultipartUpload) o;
+ return Objects.equals(key, that.key)
+ && Objects.equals(uploadId, that.uploadId)
+ && Objects.equals(storageClass, that.storageClass)
+ && Objects.equals(initiated, that.initiated);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(key, uploadId, storageClass, initiated);
+ }
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("key", key)
+ .add("uploadId", uploadId)
+ .add("storageClass", storageClass)
+ .add("initiated", initiated)
+ .toString();
+ }
+
+ /**
+ * Returns a new builder for this multipart upload.
+ *
+ * @return A new builder.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /**
+ * A builder for {@link MultipartUpload}.
+ *
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public static final class Builder {
+ private String key;
+ private String uploadId;
+ private StorageClass storageClass;
+ private OffsetDateTime initiated;
+
+ private Builder() {}
+
+ /**
+ * Sets the object name for which the multipart upload was initiated.
+ *
+ * @param key The object name.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public Builder key(String key) {
+ this.key = key;
+ return this;
+ }
+
+ /**
+ * Sets the ID of the multipart upload.
+ *
+ * @param uploadId The upload ID.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public Builder uploadId(String uploadId) {
+ this.uploadId = uploadId;
+ return this;
+ }
+
+ /**
+ * Sets the storage class of the object.
+ *
+ * @param storageClass The storage class.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public Builder storageClass(StorageClass storageClass) {
+ this.storageClass = storageClass;
+ return this;
+ }
+
+ /**
+ * Sets the date and time at which the multipart upload was initiated.
+ *
+ * @param initiated The initiation date and time.
+ * @return This builder.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public Builder initiated(OffsetDateTime initiated) {
+ this.initiated = initiated;
+ return this;
+ }
+
+ /**
+ * Builds the multipart upload.
+ *
+ * @return The built multipart upload.
+ * @since 2.61.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public MultipartUpload build() {
+ return new MultipartUpload(key, uploadId, storageClass, initiated);
+ }
+ }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/UploadPartRequest.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/UploadPartRequest.java
index 59af2df211..714afc9013 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/UploadPartRequest.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/multipartupload/model/UploadPartRequest.java
@@ -35,6 +35,7 @@ public final class UploadPartRequest {
private final int partNumber;
private final String uploadId;
@Nullable private final String crc32c;
+ private final String userProject;
private UploadPartRequest(Builder builder) {
this.bucket = builder.bucket;
@@ -42,6 +43,7 @@ private UploadPartRequest(Builder builder) {
this.partNumber = builder.partNumber;
this.uploadId = builder.uploadId;
this.crc32c = builder.crc32c;
+ this.userProject = builder.userProject;
}
/**
@@ -96,10 +98,23 @@ public String uploadId() {
*/
@BetaApi
@Nullable
- public String getCrc32c() {
+ public String crc32c() {
return crc32c;
}
+ /**
+ * Returns the user-project.
+ *
+ * @return the user-project.
+ * @see x-goog-user-project
+ * @since 2.61 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public String userProject() {
+ return userProject;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -113,12 +128,13 @@ public boolean equals(Object o) {
&& Objects.equals(bucket, that.bucket)
&& Objects.equals(key, that.key)
&& Objects.equals(uploadId, that.uploadId)
- && Objects.equals(crc32c, that.crc32c);
+ && Objects.equals(crc32c, that.crc32c)
+ && Objects.equals(userProject, that.userProject);
}
@Override
public int hashCode() {
- return Objects.hash(bucket, key, partNumber, uploadId, crc32c);
+ return Objects.hash(bucket, key, partNumber, uploadId, crc32c, userProject);
}
@Override
@@ -129,6 +145,7 @@ public String toString() {
.add("partNumber", partNumber)
.add("uploadId", uploadId)
.add("crc32c", crc32c)
+ .add("userProject", userProject)
.toString();
}
@@ -155,6 +172,7 @@ public static class Builder {
private int partNumber;
private String uploadId;
@Nullable private String crc32c;
+ private String userProject;
private Builder() {}
@@ -223,6 +241,21 @@ public Builder crc32c(@Nullable String crc32c) {
return this;
}
+ /**
+ * Sets the user-project.
+ *
+ * @param userProject The user-project.
+ * @return This builder.
+ * @see x-goog-user-project
+ * @since 2.61 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public Builder userProject(String userProject) {
+ this.userProject = userProject;
+ return this;
+ }
+
/**
* Builds the {@link UploadPartRequest}.
*
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java
index 20650a11d0..5f910fb777 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java
@@ -461,11 +461,18 @@ public Tuple> list(Map