Skip to content

Commit 8d5aef6

Browse files
committed
squashing last two commits
1 parent cc1da3c commit 8d5aef6

20 files changed

+1262
-13
lines changed

google-cloud-storage/pom.xml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,10 @@
140140
<groupId>io.opentelemetry</groupId>
141141
<artifactId>opentelemetry-api</artifactId>
142142
</dependency>
143-
143+
<dependency>
144+
<groupId>com.fasterxml.jackson.dataformat</groupId>
145+
<artifactId>jackson-dataformat-xml</artifactId>
146+
</dependency>
144147
<dependency>
145148
<groupId>io.opentelemetry</groupId>
146149
<artifactId>opentelemetry-sdk-metrics</artifactId>

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

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,13 @@
2626
import com.google.api.gax.rpc.HeaderProvider;
2727
import com.google.api.gax.tracing.ApiTracerFactory;
2828
import com.google.auth.Credentials;
29+
import com.google.cloud.storage.Retrying.RetryingDependencies;
2930
import com.google.cloud.ServiceFactory;
3031
import com.google.cloud.ServiceRpc;
3132
import com.google.cloud.TransportOptions;
3233
import com.google.cloud.http.HttpTransportOptions;
3334
import com.google.cloud.spi.ServiceRpcFactory;
3435
import com.google.cloud.storage.BlobWriteSessionConfig.WriterFactory;
35-
import com.google.cloud.storage.Retrying.DefaultRetrier;
36-
import com.google.cloud.storage.Retrying.HttpRetrier;
37-
import com.google.cloud.storage.Retrying.RetryingDependencies;
3836
import com.google.cloud.storage.Storage.BlobWriteOption;
3937
import com.google.cloud.storage.TransportCompatibility.Transport;
4038
import com.google.cloud.storage.spi.StorageRpcFactory;
@@ -407,15 +405,7 @@ public Storage create(StorageOptions options) {
407405
blobWriteSessionConfig = HttpStorageOptions.defaults().getDefaultStorageWriterConfig();
408406
}
409407
WriterFactory factory = blobWriteSessionConfig.createFactory(clock);
410-
StorageImpl storage =
411-
new StorageImpl(
412-
httpStorageOptions,
413-
factory,
414-
new HttpRetrier(
415-
new DefaultRetrier(
416-
OtelStorageDecorator.retryContextDecorator(otel),
417-
RetryingDependencies.simple(
418-
options.getClock(), options.getRetrySettings()))));
408+
StorageImpl storage = new StorageImpl(httpStorageOptions, factory, options.createRetrier());
419409
return OtelStorageDecorator.decorate(storage, otel, Transport.HTTP);
420410
} catch (IOException e) {
421411
throw new IllegalStateException(
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.storage;
18+
19+
import com.google.api.core.BetaApi;
20+
import com.google.api.core.InternalExtensionOnly;
21+
import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadRequest;
22+
import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadResponse;
23+
import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadRequest;
24+
import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadResponse;
25+
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest;
26+
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse;
27+
import com.google.cloud.storage.multipartupload.model.UploadPartRequest;
28+
import com.google.cloud.storage.multipartupload.model.UploadPartResponse;
29+
import java.io.IOException;
30+
import java.net.URI;
31+
import java.security.NoSuchAlgorithmException;
32+
33+
@BetaApi
34+
@InternalExtensionOnly
35+
public abstract class MultipartUploadClient {
36+
37+
MultipartUploadClient() {}
38+
39+
public abstract CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUploadRequest request)
40+
throws IOException;
41+
42+
public abstract UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requestBody)
43+
throws IOException;
44+
45+
public abstract CompleteMultipartUploadResponse completeMultipartUpload(
46+
CompleteMultipartUploadRequest request)
47+
throws NoSuchAlgorithmException, IOException;
48+
49+
public abstract AbortMultipartUploadResponse abortMultipartUpload(AbortMultipartUploadRequest request)
50+
throws IOException;
51+
52+
public static MultipartUploadClient create(MultipartUploadSettings config) {
53+
HttpStorageOptions options = config.getOptions();
54+
return new MultipartUploadClientImpl(
55+
URI.create(options.getHost()),
56+
options.getStorageRpcV1().getStorage().getRequestFactory(),
57+
options.createRetrier());
58+
}
59+
}
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.storage;
17+
18+
import static com.google.cloud.storage.MultipartUploadUtility.getRfc1123Date;
19+
import static com.google.cloud.storage.MultipartUploadUtility.readStream;
20+
import static com.google.cloud.storage.MultipartUploadUtility.signRequest;
21+
22+
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
23+
import com.google.api.client.http.AbstractHttpContent;
24+
import com.google.api.client.http.HttpRequestFactory;
25+
import com.google.api.client.util.ObjectParser;
26+
import com.google.api.gax.grpc.GrpcStatusCode;
27+
import com.google.api.gax.rpc.UnimplementedException;
28+
import com.google.cloud.storage.Retrying.Retrier;
29+
import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadRequest;
30+
import com.google.cloud.storage.multipartupload.model.AbortMultipartUploadResponse;
31+
import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadRequest;
32+
import com.google.cloud.storage.multipartupload.model.CompleteMultipartUploadResponse;
33+
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadRequest;
34+
import com.google.cloud.storage.multipartupload.model.CreateMultipartUploadResponse;
35+
import com.google.cloud.storage.multipartupload.model.UploadPartRequest;
36+
import com.google.cloud.storage.multipartupload.model.UploadPartResponse;
37+
import io.grpc.Status;
38+
import java.io.IOException;
39+
import java.io.InputStream;
40+
import java.io.OutputStream;
41+
import java.io.Reader;
42+
import java.lang.reflect.Type;
43+
import java.net.HttpURLConnection;
44+
import java.net.URI;
45+
import java.net.URL;
46+
import java.nio.charset.Charset;
47+
import java.nio.charset.StandardCharsets;
48+
import java.security.MessageDigest;
49+
import java.security.NoSuchAlgorithmException;
50+
import java.util.Base64;
51+
52+
public class MultipartUploadClientImpl extends MultipartUploadClient {
53+
54+
// Add HMAC keys from GCS Settings > Interoperability
55+
56+
// --- End Configuration ---
57+
private static final String GCS_ENDPOINT = "https://storage.googleapis.com";
58+
59+
public MultipartUploadClientImpl(URI uri, HttpRequestFactory requestFactory, Retrier retrier) {
60+
}
61+
62+
public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUploadRequest request)
63+
throws IOException {
64+
String resourcePath = "/" + request.bucket() + "/" + request.key();
65+
String uri = GCS_ENDPOINT + resourcePath + "?uploads";
66+
String date = getRfc1123Date();
67+
String contentType = "application/x-www-form-urlencoded";
68+
// GCS Signature Rule #1: The '?uploads' query string IS included for the initiate request.
69+
String signature = signRequest("POST", "", contentType, date, resourcePath + "?uploads", GOOGLE_SECRET_KEY);
70+
String authHeader = "GOOG1 " + GOOGLE_ACCESS_KEY + ":" + signature;
71+
72+
HttpURLConnection connection = (HttpURLConnection) new URL(uri).openConnection();
73+
connection.setRequestMethod("POST");
74+
connection.setRequestProperty("Date", date);
75+
connection.setRequestProperty("Authorization", authHeader);
76+
connection.setRequestProperty("Content-Type", contentType);
77+
connection.setFixedLengthStreamingMode(0);
78+
connection.setDoOutput(true);
79+
80+
if (connection.getResponseCode() != 200) {
81+
String error = readStream(connection.getErrorStream());
82+
throw new RuntimeException("Failed to initiate upload: " + connection.getResponseCode() + " " + error);
83+
}
84+
85+
XmlMapper xmlMapper = new XmlMapper();
86+
return xmlMapper.readValue(
87+
connection.getInputStream(), CreateMultipartUploadResponse.class);
88+
}
89+
90+
public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requestBody)
91+
throws IOException {
92+
String resourcePath = "/" + request.bucket() + "/" + request.key();
93+
String queryString = "?partNumber=" + request.partNumber() + "&uploadId=" + request.uploadId();
94+
String uri = GCS_ENDPOINT + resourcePath + queryString;
95+
String date = getRfc1123Date();
96+
String contentType = "application/octet-stream";
97+
// GCS Signature Rule #2: The query string IS NOT included for the PUT part request.
98+
String signature = signRequest("PUT", "", contentType, date, resourcePath, GOOGLE_SECRET_KEY);
99+
100+
String authHeader = "GOOG1 " + GOOGLE_ACCESS_KEY + ":" + signature;
101+
102+
HttpURLConnection connection = (HttpURLConnection) new URL(uri).openConnection();
103+
connection.setRequestMethod("PUT");
104+
connection.setRequestProperty("Date", date);
105+
connection.setRequestProperty("Authorization", authHeader);
106+
connection.setRequestProperty("Content-Type", contentType);
107+
connection.setFixedLengthStreamingMode(requestBody.getPartData().length);
108+
connection.setDoOutput(true);
109+
110+
try (OutputStream os = connection.getOutputStream()) {
111+
os.write(requestBody.getPartData());
112+
}
113+
114+
if (connection.getResponseCode() != 200) {
115+
String error = readStream(connection.getErrorStream());
116+
throw new RuntimeException("Failed to upload part " + request.partNumber() + ": " + connection.getResponseCode() + " " + error);
117+
}
118+
String eTag = connection.getHeaderField("ETag");
119+
return UploadPartResponse.builder().eTag(eTag).build();
120+
}
121+
122+
public CompleteMultipartUploadResponse completeMultipartUpload(CompleteMultipartUploadRequest request)
123+
throws NoSuchAlgorithmException, IOException {
124+
String resourcePath = "/" + request.bucket() + "/" + request.key();
125+
String queryString = "?uploadId=" + request.uploadId();
126+
String uri = GCS_ENDPOINT + resourcePath + queryString;
127+
128+
XmlMapper xmlMapper = new XmlMapper();
129+
byte[] xmlBodyBytes = xmlMapper.writeValueAsBytes(request.multipartUpload());
130+
131+
MessageDigest md = MessageDigest.getInstance("MD5");
132+
String contentMd5 = Base64.getEncoder().encodeToString(md.digest(xmlBodyBytes));
133+
String date = getRfc1123Date();
134+
String contentType = "application/xml";
135+
136+
// GCS Signature Rule #3: The query string IS NOT included for the POST complete request.
137+
String signature = signRequest("POST", contentMd5, contentType, date, resourcePath, GOOGLE_SECRET_KEY);
138+
String authHeader = "GOOG1 " + GOOGLE_ACCESS_KEY + ":" + signature;
139+
140+
HttpURLConnection connection = (HttpURLConnection) new URL(uri).openConnection();
141+
connection.setRequestMethod("POST");
142+
connection.setRequestProperty("Date", date);
143+
connection.setRequestProperty("Authorization", authHeader);
144+
connection.setRequestProperty("Content-Type", contentType);
145+
connection.setRequestProperty("Content-MD5", contentMd5);
146+
connection.setFixedLengthStreamingMode(xmlBodyBytes.length);
147+
connection.setDoOutput(true);
148+
149+
try (OutputStream os = connection.getOutputStream()) {
150+
os.write(xmlBodyBytes);
151+
}
152+
153+
if (connection.getResponseCode() != 200) {
154+
String error = readStream(connection.getErrorStream());
155+
throw new RuntimeException("Failed to complete upload: " + connection.getResponseCode() + " " + error);
156+
}
157+
return null;
158+
}
159+
160+
@Override
161+
public AbortMultipartUploadResponse abortMultipartUpload(AbortMultipartUploadRequest request) throws IOException{
162+
String resourcePath = "/" + request.bucket() + "/" + request.key();
163+
String queryString = "?uploadId=" + request.uploadId();
164+
String uri = GCS_ENDPOINT + resourcePath + queryString;
165+
String date = getRfc1123Date();
166+
167+
// GCS Signature Rule #4: The query string IS NOT included for the DELETE abort request.
168+
String signature = signRequest("DELETE", "", "", date, resourcePath, GOOGLE_SECRET_KEY);
169+
170+
String authHeader = "GOOG1 " + GOOGLE_ACCESS_KEY + ":" + signature;
171+
172+
HttpURLConnection connection = (HttpURLConnection) new URL(uri).openConnection();
173+
connection.setRequestMethod("DELETE");
174+
connection.setRequestProperty("Date", date);
175+
connection.setRequestProperty("Authorization", authHeader);
176+
177+
if (connection.getResponseCode() != 204) {
178+
String error = readStream(connection.getErrorStream());
179+
throw new RuntimeException("Failed to abort upload: " + connection.getResponseCode() + " " + error);
180+
}
181+
return new AbortMultipartUploadResponse();
182+
}
183+
184+
private static final class Utf8StringRequestContent extends AbstractHttpContent {
185+
186+
private final byte[] xml;
187+
188+
private Utf8StringRequestContent(byte[] xml) {
189+
// https://www.ietf.org/rfc/rfc2376.txt#:~:text=6.1%20text/xml%20with%20UTF%2D8%20Charset
190+
super("text/xml;charset=utf-8");
191+
this.xml = xml;
192+
}
193+
194+
@Override
195+
public long getLength() throws IOException {
196+
return super.getLength();
197+
}
198+
199+
@Override
200+
public void writeTo(OutputStream out) throws IOException {
201+
out.write(xml);
202+
}
203+
204+
public static Utf8StringRequestContent of(String xml) {
205+
return new Utf8StringRequestContent(xml.getBytes(StandardCharsets.UTF_8));
206+
}
207+
}
208+
209+
private static class XmlObjectParser implements ObjectParser {
210+
211+
@Override
212+
public <T> T parseAndClose(InputStream in, Charset charset, Class<T> dataClass)
213+
throws IOException {
214+
try (InputStream is = in) {
215+
return todo();
216+
}
217+
}
218+
219+
@Override
220+
public Object parseAndClose(InputStream in, Charset charset, Type dataType) throws IOException {
221+
try (InputStream is = in) {
222+
return todo();
223+
}
224+
}
225+
226+
@Override
227+
public <T> T parseAndClose(Reader reader, Class<T> dataClass) throws IOException {
228+
try (Reader r = reader) {
229+
return todo();
230+
}
231+
}
232+
233+
@Override
234+
public Object parseAndClose(Reader reader, Type dataType) throws IOException {
235+
try (Reader r = reader) {
236+
return todo();
237+
}
238+
}
239+
240+
private static <T> T todo() {
241+
throw new UnimplementedException("todo", null, GrpcStatusCode.of(Status.Code.UNIMPLEMENTED), false);
242+
}
243+
}
244+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.storage;
17+
18+
public final class MultipartUploadSettings {
19+
private final HttpStorageOptions options;
20+
21+
private MultipartUploadSettings(HttpStorageOptions options) {
22+
this.options = options;
23+
}
24+
25+
public HttpStorageOptions getOptions() {
26+
return options;
27+
}
28+
29+
public static MultipartUploadSettings of(HttpStorageOptions options) {
30+
return new MultipartUploadSettings(options);
31+
}
32+
}

0 commit comments

Comments
 (0)