Skip to content

Commit c0e396a

Browse files
committed
feat(gax): add ResumableUploadClient SPI and types
1 parent 7c7cb65 commit c0e396a

5 files changed

Lines changed: 419 additions & 0 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
package com.google.api.gax.resumable;
31+
32+
import com.google.api.core.InternalApi;
33+
import com.google.api.gax.rpc.UnaryCallable;
34+
import org.jspecify.annotations.NullMarked;
35+
36+
/** Client interface for executing low-level resumable upload operations. */
37+
@NullMarked
38+
@InternalApi
39+
public interface ResumableUploadClient {
40+
41+
/** Returns a {@link UnaryCallable} to initiate a resumable upload session. */
42+
UnaryCallable<StartUploadRequest, ResumableUploadSession> startUploadCallable();
43+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
package com.google.api.gax.resumable;
31+
32+
import com.google.api.core.InternalApi;
33+
import com.google.auto.value.AutoValue;
34+
import org.jspecify.annotations.NullMarked;
35+
36+
/** Represents the session metadata returned after starting a resumable upload. */
37+
@NullMarked
38+
@InternalApi
39+
@AutoValue
40+
public abstract class ResumableUploadSession {
41+
42+
private static final long DEFAULT_CHUNK_GRANULARITY = 1L;
43+
44+
/** Returns the server-provided URL to which data uploads are directed. */
45+
public abstract String getUploadUrl();
46+
47+
/**
48+
* Returns the server-mandated chunk granularity in bytes.
49+
*
50+
* <p>When specified by the server (via {@code X-Goog-Upload-Chunk-Granularity}), intermediate
51+
* upload chunks must have a size and offset that are an exact multiple of this value (the final
52+
* chunk may be smaller). If not specified by the server, this defaults to 1 byte, indicating no
53+
* alignment or granularity requirements apply.
54+
*
55+
* @return the chunk granularity in bytes
56+
*/
57+
public abstract long getChunkGranularity();
58+
59+
/**
60+
* Creates a {@link ResumableUploadSession} with the specified upload URL and default chunk
61+
* granularity.
62+
*
63+
* @param uploadUrl the upload session URL
64+
* @return a new {@link ResumableUploadSession} instance
65+
*/
66+
public static ResumableUploadSession create(String uploadUrl) {
67+
return create(uploadUrl, DEFAULT_CHUNK_GRANULARITY);
68+
}
69+
70+
/**
71+
* Creates a {@link ResumableUploadSession} with the specified upload URL and chunk granularity.
72+
*
73+
* @param uploadUrl the upload session URL
74+
* @param chunkGranularity the chunk granularity in bytes; if &le; 0, 1 is used to indicate no
75+
* alignment or granularity requirements apply.
76+
* @return a new {@link ResumableUploadSession} instance
77+
*/
78+
public static ResumableUploadSession create(String uploadUrl, long chunkGranularity) {
79+
return new AutoValue_ResumableUploadSession(
80+
uploadUrl, chunkGranularity > 0 ? chunkGranularity : DEFAULT_CHUNK_GRANULARITY);
81+
}
82+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
package com.google.api.gax.resumable;
31+
32+
import com.google.api.core.InternalApi;
33+
import com.google.auto.value.AutoValue;
34+
import com.google.common.collect.ImmutableList;
35+
import com.google.common.collect.ImmutableMap;
36+
import java.util.Collections;
37+
import java.util.List;
38+
import java.util.Map;
39+
import org.jspecify.annotations.NullMarked;
40+
import org.jspecify.annotations.Nullable;
41+
42+
/** Request parameters for initiating a resumable upload session. */
43+
@NullMarked
44+
@InternalApi
45+
@AutoValue
46+
public abstract class StartUploadRequest {
47+
48+
/** Returns the URL path to append to the service endpoint. */
49+
public abstract String getPath();
50+
51+
/** Returns the optional initial JSON request payload. */
52+
@Nullable
53+
public abstract String getJsonPayload();
54+
55+
/** Returns the query parameters for the initiation request. */
56+
public abstract Map<String, List<String>> getQueryParams();
57+
58+
public abstract Builder toBuilder();
59+
60+
public static Builder newBuilder() {
61+
return new AutoValue_StartUploadRequest.Builder().setQueryParams(Collections.emptyMap());
62+
}
63+
64+
/**
65+
* Convenience factory for creating a {@link StartUploadRequest} with only a target path.
66+
*
67+
* @param path the resource upload path
68+
* @return a new {@link StartUploadRequest} instance
69+
*/
70+
public static StartUploadRequest create(String path) {
71+
return newBuilder().setPath(path).build();
72+
}
73+
74+
@AutoValue.Builder
75+
public abstract static class Builder {
76+
public abstract Builder setPath(String path);
77+
78+
public abstract Builder setJsonPayload(@Nullable String jsonPayload);
79+
80+
public abstract Builder setQueryParams(Map<String, List<String>> queryParams);
81+
82+
abstract @Nullable Map<String, List<String>> getQueryParams();
83+
84+
abstract @Nullable String getPath();
85+
86+
abstract StartUploadRequest autoBuild();
87+
88+
public StartUploadRequest build() {
89+
if (getPath() != null && getPath().startsWith("/")) {
90+
setPath(getPath().substring(1));
91+
}
92+
93+
Map<String, List<String>> params = getQueryParams();
94+
if (params != null && !params.isEmpty()) {
95+
ImmutableMap.Builder<String, List<String>> mapBuilder = ImmutableMap.builder();
96+
for (Map.Entry<String, List<String>> entry : params.entrySet()) {
97+
mapBuilder.put(entry.getKey(), ImmutableList.copyOf(entry.getValue()));
98+
}
99+
setQueryParams(mapBuilder.build());
100+
} else {
101+
setQueryParams(Collections.emptyMap());
102+
}
103+
104+
return autoBuild();
105+
}
106+
}
107+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
package com.google.api.gax.resumable;
31+
32+
import static com.google.common.truth.Truth.assertThat;
33+
34+
import org.junit.jupiter.api.Test;
35+
import org.junit.jupiter.params.ParameterizedTest;
36+
import org.junit.jupiter.params.provider.ValueSource;
37+
38+
class ResumableUploadSessionTest {
39+
40+
private static final String UPLOAD_URL = "https://storage.googleapis.com/upload/session/12345";
41+
42+
@Test
43+
void create_withUploadUrl_setsDefaultChunkGranularityToOneByte() {
44+
ResumableUploadSession session = ResumableUploadSession.create(UPLOAD_URL);
45+
46+
assertThat(session.getUploadUrl()).isEqualTo(UPLOAD_URL);
47+
assertThat(session.getChunkGranularity()).isEqualTo(1L);
48+
}
49+
50+
@Test
51+
void create_withExplicitChunkGranularity_preservesGranularity() {
52+
long customChunkGranularity = 256 * 1024L;
53+
54+
ResumableUploadSession session =
55+
ResumableUploadSession.create(UPLOAD_URL, customChunkGranularity);
56+
57+
assertThat(session.getUploadUrl()).isEqualTo(UPLOAD_URL);
58+
assertThat(session.getChunkGranularity()).isEqualTo(customChunkGranularity);
59+
}
60+
61+
@ParameterizedTest
62+
@ValueSource(longs = {0L, -1L, -100L})
63+
void create_withNonPositiveChunkGranularity_normalizesToOneByte(long invalidGranularity) {
64+
ResumableUploadSession session =
65+
ResumableUploadSession.create(UPLOAD_URL, invalidGranularity);
66+
67+
assertThat(session.getChunkGranularity()).isEqualTo(1L);
68+
}
69+
}

0 commit comments

Comments
 (0)