diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ChunkUploadRequest.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ChunkUploadRequest.java new file mode 100644 index 000000000000..b385a0a1b8a0 --- /dev/null +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ChunkUploadRequest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.rpc; + +import com.google.api.core.BetaApi; +import com.google.common.base.Preconditions; + +/** Value object representing a chunk upload request. */ +@BetaApi +public class ChunkUploadRequest { + + private final String uploadUrl; + private final byte[] data; + private final long offset; + private final long totalSize; + private final boolean isLast; + + public ChunkUploadRequest( + String uploadUrl, byte[] data, long offset, long totalSize, boolean isLast) { + this.uploadUrl = Preconditions.checkNotNull(uploadUrl); + this.data = Preconditions.checkNotNull(data); + this.offset = offset; + this.totalSize = totalSize; + this.isLast = isLast; + } + + public String getUploadUrl() { + return uploadUrl; + } + + public byte[] getData() { + return data; + } + + public long getOffset() { + return offset; + } + + public long getTotalSize() { + return totalSize; + } + + public boolean isLast() { + return isLast; + } +} diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ChunkUploadResponse.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ChunkUploadResponse.java new file mode 100644 index 000000000000..f62b537a9633 --- /dev/null +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ChunkUploadResponse.java @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.rpc; + +import com.google.api.core.BetaApi; +import javax.annotation.Nullable; + +/** Value object representing a chunk upload response. */ +@BetaApi +public class ChunkUploadResponse { + + private final long committedOffset; + @Nullable private final Object responseBody; + + public ChunkUploadResponse(long committedOffset, @Nullable Object responseBody) { + this.committedOffset = committedOffset; + this.responseBody = responseBody; + } + + public long getCommittedOffset() { + return committedOffset; + } + + @Nullable + public Object getResponseBody() { + return responseBody; + } +} diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadCallSettings.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadCallSettings.java new file mode 100644 index 000000000000..05e3e8382c49 --- /dev/null +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadCallSettings.java @@ -0,0 +1,76 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.rpc; + +import com.google.api.core.BetaApi; +import com.google.auto.value.AutoValue; + +/** + * A settings class to configure a {@link ResumableUploadCallable} for executing resumable uploads. + * Encapsulates protocol options such as payload chunk size. + */ +@BetaApi +@AutoValue +public abstract class ResumableUploadCallSettings { + private static final int DEFAULT_CHUNK_SIZE = 8 * 1024 * 1024; // 8 MB + + /** Returns the configured chunk size in bytes (defaults to 8 MB / 8,388,608 bytes). */ + public abstract int getChunkSize(); + + /** + * Merges another {@code ResumableUploadCallSettings} instance with this one. Fields set in {@code + * other} override fields in this instance. + * + * @param other settings to overlay; may be {@code null} + * @return a new, resolved {@code ResumableUploadCallSettings} instance + */ + public ResumableUploadCallSettings merge(ResumableUploadCallSettings other) { + if (other == null) { + return this; + } + return toBuilder().setChunkSize(other.getChunkSize()).build(); + } + + public abstract Builder toBuilder(); + + public static Builder newBuilder() { + return new AutoValue_ResumableUploadCallSettings.Builder().setChunkSize(DEFAULT_CHUNK_SIZE); + } + + /** Builder for {@link ResumableUploadCallSettings}. */ + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setChunkSize(int chunkSize); + + public abstract int getChunkSize(); + + public abstract ResumableUploadCallSettings build(); + } +} diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadCallable.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadCallable.java new file mode 100644 index 000000000000..1104f76942f8 --- /dev/null +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadCallable.java @@ -0,0 +1,128 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.rpc; + +import com.google.api.core.BetaApi; +import com.google.common.base.Preconditions; +import java.io.InputStream; +import javax.annotation.Nullable; + +/** + * A ResumableUploadCallable is an API-transport-independent wrapper for the Resumable Upload + * protocol. Operates directly on the request object and input stream payload. + * + * @param request type + * @param response type + */ +@BetaApi +public class ResumableUploadCallable { + + private final ResumableUploadClient resumableUploadClient; + @Nullable private final ResumableUploadCallSettings defaultCallSettings; + + public ResumableUploadCallable( + ResumableUploadClient resumableUploadClient, + @Nullable ResumableUploadCallSettings defaultCallSettings) { + this.resumableUploadClient = Preconditions.checkNotNull(resumableUploadClient); + this.defaultCallSettings = defaultCallSettings; + } + + public ResumableUploadCallable(ResumableUploadClient resumableUploadClient) { + this(resumableUploadClient, null); + } + + /** + * Performs a new resumable upload asynchronously. + * + * @param request the request message + * @param payload the data payload input stream + * @param perRequestSettings call settings overrides; may be {@code null} + * @param context call context overrides; may be {@code null} + * @return future for tracking and controlling the upload + */ + public ResumableUploadFuture futureCall( + RequestT request, + InputStream payload, + ResumableUploadCallSettings perRequestSettings, + ApiCallContext context) { + Preconditions.checkNotNull(request); + + ResumableUploadCallSettings activeSettings = + defaultCallSettings != null + ? defaultCallSettings.merge(perRequestSettings) + : perRequestSettings; + + ResumableUploadFutureImpl future = + new ResumableUploadFutureImpl<>( + resumableUploadClient, request, payload, activeSettings, context); + + future.start(); + return future; + } + + /** + * Resumes an existing resumable upload session asynchronously using a saved session URL. + * + * @param sessionUrl the upload session URL + * @param payload the data payload input stream + * @param perRequestSettings call settings overrides; may be {@code null} + * @param context call context overrides; may be {@code null} + * @return future for tracking and controlling the upload + */ + public ResumableUploadFuture resumeCall( + String sessionUrl, + InputStream payload, + ResumableUploadCallSettings perRequestSettings, + ApiCallContext context) { + Preconditions.checkNotNull(sessionUrl); + + ResumableUploadCallSettings activeSettings = + defaultCallSettings != null + ? defaultCallSettings.merge(perRequestSettings) + : perRequestSettings; + + ResumableUploadFutureImpl future = + new ResumableUploadFutureImpl<>( + resumableUploadClient, sessionUrl, payload, activeSettings, context); + + future.start(); + return future; + } + + public ResumableUploadFuture futureCall( + RequestT request, InputStream payload, ResumableUploadCallSettings settings) { + return futureCall(request, payload, settings, null); + } + + public ResumableUploadFuture resumeCall( + String sessionUrl, InputStream payload, ResumableUploadCallSettings settings) { + return resumeCall(sessionUrl, payload, settings, null); + } +} diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadClient.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadClient.java new file mode 100644 index 000000000000..949f1e1b3152 --- /dev/null +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadClient.java @@ -0,0 +1,46 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.rpc; + +import com.google.api.core.BetaApi; + +/** + * Low-level transport Service Provider Interface (SPI) for executing individual Scotty resumable + * upload operations (session initiation and chunk upload). + */ +@BetaApi +public interface ResumableUploadClient { + + /** Returns a UnaryCallable to initiate a new resumable upload session. */ + UnaryCallable startUploadCallable(); + + /** Returns a UnaryCallable to upload a payload byte chunk to an active session URL. */ + UnaryCallable uploadChunkCallable(); +} diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadFuture.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadFuture.java new file mode 100644 index 000000000000..ea2605e3a5d7 --- /dev/null +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadFuture.java @@ -0,0 +1,45 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.rpc; + +import com.google.api.core.ApiFuture; +import com.google.api.core.BetaApi; + +/** + * A specialized {@link ApiFuture} for tracking and controlling an in-flight resumable upload. + * + * @param response type + */ +@BetaApi +public interface ResumableUploadFuture extends ApiFuture { + + /** Returns the upload session URL, or {@code null} if session initiation is in progress. */ + String getUploadSessionUrl(); +} diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadFutureImpl.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadFutureImpl.java new file mode 100644 index 000000000000..3405037adeeb --- /dev/null +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadFutureImpl.java @@ -0,0 +1,190 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.rpc; + +import com.google.api.core.AbstractApiFuture; +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.api.core.BetaApi; +import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.MoreExecutors; +import java.io.InputStream; +import javax.annotation.Nullable; + +/** + * Stateful per-request implementation of {@link ResumableUploadFuture}. Manages payload chunking, + * stream offsets, and session tracking. + * + * @param request type + * @param response type + */ +@BetaApi +public class ResumableUploadFutureImpl extends AbstractApiFuture + implements ResumableUploadFuture { + + private final ResumableUploadClient resumableUploadClient; + @Nullable private final RequestT request; + @Nullable private final String initialSessionUrl; + private final InputStream payload; + private final ResumableUploadCallSettings settings; + private final ApiCallContext context; + + private volatile String uploadSessionUrl; + private volatile long committedOffset = 0L; + private volatile ApiFuture inFlightFuture; + + public ResumableUploadFutureImpl( + ResumableUploadClient resumableUploadClient, + RequestT request, + InputStream payload, + ResumableUploadCallSettings settings, + ApiCallContext context) { + this.resumableUploadClient = Preconditions.checkNotNull(resumableUploadClient); + this.request = Preconditions.checkNotNull(request); + this.initialSessionUrl = null; + this.payload = Preconditions.checkNotNull(payload); + this.settings = settings; + this.context = context; + } + + public ResumableUploadFutureImpl( + ResumableUploadClient resumableUploadClient, + String sessionUrl, + InputStream payload, + ResumableUploadCallSettings settings, + ApiCallContext context) { + this.resumableUploadClient = Preconditions.checkNotNull(resumableUploadClient); + this.request = null; + this.initialSessionUrl = Preconditions.checkNotNull(sessionUrl); + this.payload = Preconditions.checkNotNull(payload); + this.settings = settings; + this.context = context; + } + + public void start() { + if (initialSessionUrl != null) { + this.uploadSessionUrl = initialSessionUrl; + uploadNextChunk(); + } else { + initiateSessionAndUpload(); + } + } + + private void initiateSessionAndUpload() { + ApiFuture sessionFuture = + resumableUploadClient.startUploadCallable().futureCall(request, context); + this.inFlightFuture = sessionFuture; + + ApiFutures.addCallback( + sessionFuture, + new ApiFutureCallback() { + @Override + public void onSuccess(ResumableUploadSession session) { + uploadSessionUrl = session.getUploadUrl(); + uploadNextChunk(); + } + + @Override + public void onFailure(Throwable t) { + setException(t); + } + }, + MoreExecutors.directExecutor()); + } + + private void uploadNextChunk() { + try { + int chunkSize = settings != null ? settings.getChunkSize() : 8 * 1024 * 1024; + byte[] buffer = new byte[chunkSize]; + int bytesRead = payload.read(buffer); + + if (bytesRead == -1) { + bytesRead = 0; + } + + byte[] chunkData; + if (bytesRead < chunkSize) { + chunkData = new byte[bytesRead]; + System.arraycopy(buffer, 0, chunkData, 0, bytesRead); + } else { + chunkData = buffer; + } + + ChunkUploadRequest chunkRequest = + new ChunkUploadRequest(uploadSessionUrl, chunkData, committedOffset, -1L, bytesRead == 0); + + ApiFuture chunkFuture = + resumableUploadClient.uploadChunkCallable().futureCall(chunkRequest, context); + this.inFlightFuture = chunkFuture; + + ApiFutures.addCallback( + chunkFuture, + new ApiFutureCallback() { + @Override + public void onSuccess(ChunkUploadResponse response) { + committedOffset = response.getCommittedOffset(); + if (response.getResponseBody() != null) { + @SuppressWarnings("unchecked") + ResponseT result = (ResponseT) response.getResponseBody(); + set(result); + } else { + uploadNextChunk(); + } + } + + @Override + public void onFailure(Throwable t) { + setException(t); + } + }, + MoreExecutors.directExecutor()); + } catch (Throwable t) { + setException(t); + } + } + + @Override + public String getUploadSessionUrl() { + return uploadSessionUrl; + } + + public long getCommittedOffset() { + return committedOffset; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + if (inFlightFuture != null) { + inFlightFuture.cancel(mayInterruptIfRunning); + } + return super.cancel(mayInterruptIfRunning); + } +} diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadSession.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadSession.java new file mode 100644 index 000000000000..128dde04bace --- /dev/null +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResumableUploadSession.java @@ -0,0 +1,48 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.rpc; + +import com.google.api.core.BetaApi; +import com.google.common.base.Preconditions; + +/** Value object representing an initiated resumable upload session. */ +@BetaApi +public class ResumableUploadSession { + + private final String uploadUrl; + + public ResumableUploadSession(String uploadUrl) { + this.uploadUrl = Preconditions.checkNotNull(uploadUrl); + } + + public String getUploadUrl() { + return uploadUrl; + } +} diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/ResumableUploadCallSettingsTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/ResumableUploadCallSettingsTest.java new file mode 100644 index 000000000000..d890c61e4ba0 --- /dev/null +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/ResumableUploadCallSettingsTest.java @@ -0,0 +1,77 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.rpc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.junit.jupiter.api.Test; + +public class ResumableUploadCallSettingsTest { + + @Test + public void testDefaultChunkSizeInBuilder() { + ResumableUploadCallSettings settings = ResumableUploadCallSettings.newBuilder().build(); + + assertEquals(8 * 1024 * 1024, settings.getChunkSize()); + } + + @Test + public void testCustomInitialization() { + ResumableUploadCallSettings settings = + ResumableUploadCallSettings.newBuilder().setChunkSize(16 * 1024 * 1024).build(); + + assertEquals(16 * 1024 * 1024, settings.getChunkSize()); + } + + @Test + public void testMerge_NullSettings() { + ResumableUploadCallSettings stubSettings = + ResumableUploadCallSettings.newBuilder().setChunkSize(4 * 1024 * 1024).build(); + + ResumableUploadCallSettings merged = stubSettings.merge(null); + + assertSame(stubSettings, merged); + } + + @Test + public void testMerge_SettingsOverrides() { + ResumableUploadCallSettings stubSettings = + ResumableUploadCallSettings.newBuilder().setChunkSize(4 * 1024 * 1024).build(); + + ResumableUploadCallSettings perRequestSettings = + ResumableUploadCallSettings.newBuilder().setChunkSize(32 * 1024 * 1024).build(); + + ResumableUploadCallSettings merged = stubSettings.merge(perRequestSettings); + + // Chunk size overridden by Tier-1 per-request settings + assertEquals(32 * 1024 * 1024, merged.getChunkSize()); + } +}