Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
cbad1ca
chore: Refactor retrier creation from HttpStorageOptions to StorageOp…
ShreyasSinha Oct 14, 2025
d6f4cc9
chore: Refactor retrier creation from HttpStorageOptions to StorageOp…
ShreyasSinha Oct 14, 2025
bdbb55f
feat: Added API for CreateMultipartUpload.
ShreyasSinha Oct 21, 2025
a1f6efa
chore: Merge branch 'mpu-feature-1' into mpu-feature-2
ShreyasSinha Oct 21, 2025
3bfd51b
added missing javadoc
ShreyasSinha Oct 21, 2025
1157004
Fixed lint issues
ShreyasSinha Oct 21, 2025
585707d
chore: adding review changes.
ShreyasSinha Oct 22, 2025
eca5d54
chore: addind review comment fixes
ShreyasSinha Oct 22, 2025
04ff3d5
chore: added test cases
ShreyasSinha Oct 22, 2025
f4e30f9
chore: fixed IT tests
ShreyasSinha Oct 22, 2025
86a9ab9
removing mockito conifg no longer required
ShreyasSinha Oct 22, 2025
1863d02
chore: added missing library
ShreyasSinha Oct 22, 2025
a35e4d6
feat: Adding API for ListParts
ShreyasSinha Oct 22, 2025
07afc4e
chore: adding logic for list parts.
ShreyasSinha Oct 22, 2025
48e1a2f
chore: adding missing final statement
ShreyasSinha Oct 22, 2025
81648dc
fixing licence number
ShreyasSinha Oct 22, 2025
2a5d1a0
fixing lint error
ShreyasSinha Oct 22, 2025
21c5c94
chore: adding beta api annotation.
ShreyasSinha Oct 22, 2025
fffd81b
chore: added missing annotation
ShreyasSinha Oct 22, 2025
744db03
chore: adding fixes for review comments
ShreyasSinha Oct 23, 2025
3757fed
chore: Removed merge conflicts.
ShreyasSinha Oct 23, 2025
08b084f
chore: Adding lint issues.
ShreyasSinha Oct 23, 2025
e565af6
chore: Adding fixes for IT.
ShreyasSinha Oct 23, 2025
eaff90e
Merge branch 'mpu-feature-2' into mpu-feature-3
ShreyasSinha Oct 23, 2025
0b378af
added test
ShreyasSinha Oct 23, 2025
b8530f7
chore: Adding review comments
ShreyasSinha Oct 24, 2025
956c595
Adding review comments
ShreyasSinha Oct 24, 2025
b8437d6
chore: Adding missing BetaApi annotation.
ShreyasSinha Oct 24, 2025
1ec5ead
chore:fixing lint error.
ShreyasSinha Oct 24, 2025
c8fe7fe
Merged feature 2 into feature 3
ShreyasSinha Oct 24, 2025
5c7c6ca
chore: merged feature 2 into feature 3
ShreyasSinha Oct 24, 2025
daaad24
choere: coverted Integer to int.
ShreyasSinha Oct 26, 2025
b261690
chore: fixed datatypes of few variables
ShreyasSinha Oct 26, 2025
6acb2a2
chore:adding handling for storage class nad owner
ShreyasSinha Oct 26, 2025
ddfa9fd
chore: Removed unwanted library decleration
ShreyasSinha Oct 26, 2025
337f972
chore: Adding seperate test for all use case of List parts.
ShreyasSinha Oct 26, 2025
c7549b1
chore: adding beta api annotation.
ShreyasSinha Oct 28, 2025
3869259
chore: added review comments
ShreyasSinha Oct 30, 2025
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions google-cloud-storage/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@
<pubsub-proto.version>1.123.5</pubsub-proto.version>
</properties>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@
import com.google.cloud.http.HttpTransportOptions;
import com.google.cloud.spi.ServiceRpcFactory;
import com.google.cloud.storage.BlobWriteSessionConfig.WriterFactory;
import com.google.cloud.storage.Retrying.DefaultRetrier;
import com.google.cloud.storage.Retrying.HttpRetrier;
import com.google.cloud.storage.Retrying.RetryingDependencies;
import com.google.cloud.storage.Storage.BlobWriteOption;
import com.google.cloud.storage.TransportCompatibility.Transport;
Expand Down Expand Up @@ -408,14 +406,7 @@ public Storage create(StorageOptions options) {
}
WriterFactory factory = blobWriteSessionConfig.createFactory(clock);
StorageImpl storage =
new StorageImpl(
httpStorageOptions,
factory,
new HttpRetrier(
new DefaultRetrier(
OtelStorageDecorator.retryContextDecorator(otel),
RetryingDependencies.simple(
options.getClock(), options.getRetrySettings()))));
new StorageImpl(httpStorageOptions, factory, options.createRetrier());
return OtelStorageDecorator.decorate(storage, otel, Transport.HTTP);
} catch (IOException e) {
throw new IllegalStateException(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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;

import com.google.api.core.BetaApi;
import com.google.api.core.InternalExtensionOnly;
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest;
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse;
import com.google.cloud.storage.multipartupload.model.ListPartsRequest;
import com.google.cloud.storage.multipartupload.model.ListPartsResponse;
import java.io.IOException;
import java.net.URI;

/**
* A client for interacting with Google Cloud Storage's Multipart Upload API.
*
* <p>This class is for internal use only and is not intended for public consumption. It provides a
* low-level interface for creating and managing multipart uploads.
*
* @see <a href="https://cloud.google.com/storage/docs/multipart-uploads">Multipart Uploads</a>
*/
@BetaApi
@InternalExtensionOnly
public abstract class MultipartUploadClient {

MultipartUploadClient() {}

/**
* Creates a new multipart upload.
*
* @param request The request object containing the details for creating the multipart upload.
* @return A {@link CreateMultipartUploadResponse} object containing the upload ID.
* @throws IOException if an I/O error occurs.
*/
@BetaApi
public abstract CreateMultipartUploadResponse createMultipartUpload(
CreateMultipartUploadRequest request) throws IOException;

/**
* Lists the parts that have been uploaded for a specific multipart upload.
*
* @param listPartsRequest The request object containing the details for listing the parts.
* @return A {@link ListPartsResponse} object containing the list of parts.
*/
@BetaApi
public abstract ListPartsResponse listParts(ListPartsRequest listPartsRequest);

/**
* Creates a new instance of {@link MultipartUploadClient}.
*
* @param config The configuration for the client.
* @return A new {@link MultipartUploadClient} instance.
*/
@BetaApi
public static MultipartUploadClient create(MultipartUploadSettings config) {
HttpStorageOptions options = config.getOptions();
return new MultipartUploadClientImpl(
URI.create(options.getHost()),
options.createRetrier(),
MultipartUploadHttpRequestManager.createFrom(options),
options.getRetryAlgorithmManager());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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;

import com.google.api.core.BetaApi;
import com.google.cloud.storage.Conversions.Decoder;
import com.google.cloud.storage.Retrying.Retrier;
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest;
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse;
import com.google.cloud.storage.multipartupload.model.ListPartsRequest;
import com.google.cloud.storage.multipartupload.model.ListPartsResponse;
import java.io.IOException;
import java.net.URI;

/**
* This class is an implementation of {@link MultipartUploadClient} that uses the Google Cloud
* Storage XML API to perform multipart uploads.
*/
@BetaApi
final class MultipartUploadClientImpl extends MultipartUploadClient {

private final MultipartUploadHttpRequestManager httpRequestManager;
private final Retrier retrier;
private final URI uri;
private final HttpRetryAlgorithmManager retryAlgorithmManager;

MultipartUploadClientImpl(
URI uri,
Retrier retrier,
MultipartUploadHttpRequestManager multipartUploadHttpRequestManager,
HttpRetryAlgorithmManager retryAlgorithmManager) {
this.httpRequestManager = multipartUploadHttpRequestManager;
this.retrier = retrier;
this.uri = uri;
this.retryAlgorithmManager = retryAlgorithmManager;
}

@Override
@BetaApi
public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUploadRequest request)
throws IOException {
return httpRequestManager.sendCreateMultipartUploadRequest(uri, request);
}

@Override
@BetaApi
public ListPartsResponse listParts(ListPartsRequest request) {

return retrier.run(
retryAlgorithmManager.idempotent(),
() -> httpRequestManager.sendListPartsRequest(uri, request),
Decoder.identity());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* 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;

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;
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.util.ObjectParser;
import com.google.api.gax.core.GaxProperties;
import com.google.api.gax.rpc.FixedHeaderProvider;
import com.google.api.gax.rpc.HeaderProvider;
import com.google.api.services.storage.Storage;
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest;
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse;
import com.google.cloud.storage.multipartupload.model.ListPartsRequest;
import com.google.cloud.storage.multipartupload.model.ListPartsResponse;
import com.google.common.base.StandardSystemProperty;
import com.google.common.collect.ImmutableMap;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

final class MultipartUploadHttpRequestManager {

private final HttpRequestFactory requestFactory;
private final ObjectParser objectParser;
private final HeaderProvider headerProvider;

MultipartUploadHttpRequestManager(
HttpRequestFactory requestFactory, ObjectParser objectParser, HeaderProvider headerProvider) {
this.requestFactory = requestFactory;
this.objectParser = objectParser;
this.headerProvider = headerProvider;
}

CreateMultipartUploadResponse sendCreateMultipartUploadRequest(
URI uri, CreateMultipartUploadRequest request) throws IOException {

String encodedBucket = urlEncode(request.bucket());
String encodedKey = urlEncode(request.key());
String resourcePath = "/" + encodedBucket + "/" + encodedKey;
String createUri = uri.toString() + resourcePath + "?uploads";

HttpRequest httpRequest =
requestFactory.buildPostRequest(
new GenericUrl(createUri), new ByteArrayContent(request.getContentType(), new byte[0]));
httpRequest.getHeaders().putAll(headerProvider.getHeaders());
addHeadersForCreateMultipartUpload(request, httpRequest.getHeaders());
httpRequest.setParser(objectParser);
httpRequest.setThrowExceptionOnExecuteError(true);
return httpRequest.execute().parseAs(CreateMultipartUploadResponse.class);
}

ListPartsResponse sendListPartsRequest(URI uri, ListPartsRequest request) throws IOException {

String encodedBucket = urlEncode(request.bucket());
String encodedKey = urlEncode(request.key());
String resourcePath = "/" + encodedBucket + "/" + encodedKey;
String queryString = "?uploadId=" + urlEncode(request.uploadId());

if (request.getMaxParts() != null) {
queryString += "&max-parts=" + request.getMaxParts();
}
if (request.getPartNumberMarker() != null) {
queryString += "&part-number-marker=" + request.getPartNumberMarker();
}
String listUri = uri.toString() + resourcePath + queryString;
HttpRequest httpRequest = requestFactory.buildGetRequest(new GenericUrl(listUri));
httpRequest.getHeaders().putAll(headerProvider.getHeaders());
httpRequest.setParser(objectParser);
httpRequest.setThrowExceptionOnExecuteError(true);
return httpRequest.execute().parseAs(ListPartsResponse.class);
}

@SuppressWarnings("DataFlowIssue")
static MultipartUploadHttpRequestManager createFrom(HttpStorageOptions options) {
Storage storage = options.getStorageRpcV1().getStorage();
ImmutableMap.Builder<String, String> stableHeaders =
ImmutableMap.<String, String>builder()
// http-java-client will automatically append its own version to the user-agent
.put("User-Agent", "gcloud-java/" + options.getLibraryVersion())
.put(
"x-goog-api-client",
String.format(
"gl-java/%s gccl/%s %s/%s",
GaxProperties.getJavaVersion(),
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())));
}

private void addHeadersForCreateMultipartUpload(
CreateMultipartUploadRequest request, HttpHeaders headers) {
// TODO(shreyassinha): add a PredefinedAcl::getXmlEntry with the corresponding value from
// https://cloud.google.com/storage/docs/xml-api/reference-headers#xgoogacl
if (request.getCannedAcl() != null) {
headers.put("x-goog-acl", request.getCannedAcl().toString());
}
// TODO(shreyassinha) Add encoding for x-goog-meta-* headers
if (request.getMetadata() != null) {
for (Map.Entry<String, String> entry : request.getMetadata().entrySet()) {
if (entry.getKey() != null || entry.getValue() != null) {
headers.put("x-goog-meta-" + entry.getKey(), entry.getValue());
}
}
}
if (request.getStorageClass() != null) {
headers.put("x-goog-storage-class", request.getStorageClass().toString());
}
if (request.getKmsKeyName() != null && !request.getKmsKeyName().isEmpty()) {
headers.put("x-goog-encryption-kms-key-name", request.getKmsKeyName());
}
if (request.getObjectLockMode() != null) {
headers.put("x-goog-object-lock-mode", request.getObjectLockMode().toString());
}
if (request.getObjectLockRetainUntilDate() != null) {
headers.put(
"x-goog-object-lock-retain-until-date",
Utils.offsetDateTimeRfc3339Codec.encode(request.getObjectLockRetainUntilDate()));
}
if (request.getCustomTime() != null) {
headers.put(
"x-goog-custom-time", Utils.offsetDateTimeRfc3339Codec.encode(request.getCustomTime()));
}
}

private static String urlEncode(String value) throws UnsupportedEncodingException {
return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
}

private static String formatName(String name) {
// Only lowercase letters, digits, and "-" are allowed
return name.toLowerCase().replaceAll("[^\\w\\d\\-]", "-");
}

private static String formatSemver(String version) {
return formatSemver(version, version);
}

private static String formatSemver(String version, String defaultValue) {
if (version == null) {
return null;
}

// Take only the semver version: x.y.z-a_b_c -> x.y.z
Matcher m = Pattern.compile("(\\d+\\.\\d+\\.\\d+).*").matcher(version);
if (m.find()) {
return m.group(1);
} else {
return defaultValue;
}
}
}
Loading