Skip to content

Commit 4cba429

Browse files
committed
Initial Commit
1 parent cc1da3c commit 4cba429

12 files changed

+525
-0
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
17+
package com.google.cloud.storage.multipartuploader;
18+
19+
import com.google.cloud.storage.multipartuploader.data.CompleteMultipartRequest;
20+
import com.google.cloud.storage.multipartuploader.data.CompleteMultipartResponse;
21+
import com.google.cloud.storage.multipartuploader.data.CreateMultipartUploadRequest;
22+
import com.google.cloud.storage.multipartuploader.data.CreateMultipartUploadResponse;
23+
import com.google.cloud.storage.multipartuploader.data.RequestBody;
24+
import com.google.cloud.storage.multipartuploader.data.UploadPartRequest;
25+
import com.google.cloud.storage.multipartuploader.data.UploadPartResponse;
26+
import java.io.IOException;
27+
import java.net.MalformedURLException;
28+
import java.net.ProtocolException;
29+
import java.security.NoSuchAlgorithmException;
30+
31+
public interface MultipartUploader {
32+
33+
CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUploadRequest request)
34+
throws IOException;
35+
36+
UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requestBody)
37+
throws IOException;
38+
39+
CompleteMultipartResponse completeMultipartUpload(CompleteMultipartRequest request)
40+
throws NoSuchAlgorithmException;
41+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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.multipartuploader;
17+
18+
public class MultipartUploaderConfig {
19+
20+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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.multipartuploader;
17+
18+
import static com.google.cloud.storage.multipartuploader.MultipartUploaderUtility.getRfc1123Date;
19+
import static com.google.cloud.storage.multipartuploader.MultipartUploaderUtility.readStream;
20+
import static com.google.cloud.storage.multipartuploader.MultipartUploaderUtility.signRequest;
21+
22+
import com.google.cloud.storage.multipartuploader.data.CompleteMultipartRequest;
23+
import com.google.cloud.storage.multipartuploader.data.CompleteMultipartResponse;
24+
import com.google.cloud.storage.multipartuploader.data.CompletedPart;
25+
import com.google.cloud.storage.multipartuploader.data.CreateMultipartUploadRequest;
26+
import com.google.cloud.storage.multipartuploader.data.CreateMultipartUploadResponse;
27+
import com.google.cloud.storage.multipartuploader.data.RequestBody;
28+
import com.google.cloud.storage.multipartuploader.data.UploadPartRequest;
29+
import com.google.cloud.storage.multipartuploader.data.UploadPartResponse;
30+
import java.io.IOException;
31+
import java.io.OutputStream;
32+
import java.net.HttpURLConnection;
33+
import java.net.MalformedURLException;
34+
import java.net.ProtocolException;
35+
import java.net.URL;
36+
import java.nio.charset.StandardCharsets;
37+
import java.security.MessageDigest;
38+
import java.security.NoSuchAlgorithmException;
39+
import java.util.Base64;
40+
41+
public class MultipartUploaderImpl implements MultipartUploader {
42+
43+
private static final String BUCKET_NAME = "shreyassinha";
44+
private static final String OBJECT_NAME = "5mb"; // The name for the object in GCS
45+
private static final String FILE_PATH = "5mb-examplefile-com.txt"; // The local file to upload
46+
47+
// Add HMAC keys from GCS Settings > Interoperability
48+
49+
// --- End Configuration ---
50+
private static final String GCS_ENDPOINT = "https://storage.googleapis.com";
51+
52+
public CreateMultipartUploadResponse createMultipartUpload(CreateMultipartUploadRequest request)
53+
throws IOException {
54+
//String resourcePath = "/" + request.getBucketName() + "/" + request.getObjectName();
55+
String resourcePath = "/" + BUCKET_NAME + "/" + OBJECT_NAME;
56+
String uri = GCS_ENDPOINT + resourcePath + "?uploads";
57+
String date = getRfc1123Date();
58+
String contentType = "application/x-www-form-urlencoded";
59+
// GCS Signature Rule #1: The '?uploads' query string IS included for the initiate request.
60+
String signature = signRequest("POST", "", contentType, date, resourcePath + "?uploads", GOOGLE_SECRET_KEY);
61+
String authHeader = "GOOG1 " + GOOGLE_ACCESS_KEY + ":" + signature;
62+
63+
HttpURLConnection connection = (HttpURLConnection) new URL(uri).openConnection();
64+
connection.setRequestMethod("POST");
65+
connection.setRequestProperty("Date", date);
66+
connection.setRequestProperty("Authorization", authHeader);
67+
connection.setRequestProperty("Content-Type", contentType);
68+
connection.setFixedLengthStreamingMode(0);
69+
connection.setDoOutput(true);
70+
71+
if (connection.getResponseCode() != 200) {
72+
String error = readStream(connection.getErrorStream());
73+
throw new RuntimeException("Failed to initiate upload: " + connection.getResponseCode() + " " + error);
74+
}
75+
76+
String responseBody = readStream(connection.getInputStream());
77+
String uploadIdTag = "<UploadId>";
78+
int start = responseBody.indexOf(uploadIdTag) + uploadIdTag.length();
79+
int end = responseBody.indexOf("</UploadId>");
80+
int uploadId = Integer.parseInt(responseBody.substring(start, end));
81+
return new CreateMultipartUploadResponse(uploadId);
82+
}
83+
84+
public UploadPartResponse uploadPart(UploadPartRequest request, RequestBody requestBody)
85+
throws IOException {
86+
String resourcePath = "/" + BUCKET_NAME + "/" + OBJECT_NAME;
87+
String queryString = "?partNumber=" + request.getPartNumber() + "&uploadId=" + request.getUploadId();
88+
String uri = GCS_ENDPOINT + resourcePath + queryString;
89+
String date = getRfc1123Date();
90+
String contentType = "application/octet-stream";
91+
// GCS Signature Rule #2: The query string IS NOT included for the PUT part request.
92+
String signature = signRequest("PUT", "", contentType, date, resourcePath, GOOGLE_SECRET_KEY);
93+
94+
String authHeader = "GOOG1 " + GOOGLE_ACCESS_KEY + ":" + signature;
95+
96+
HttpURLConnection connection = (HttpURLConnection) new URL(uri).openConnection();
97+
connection.setRequestMethod("PUT");
98+
connection.setRequestProperty("Date", date);
99+
connection.setRequestProperty("Authorization", authHeader);
100+
connection.setRequestProperty("Content-Type", contentType);
101+
connection.setFixedLengthStreamingMode(requestBody.getPartDate().length);
102+
connection.setDoOutput(true);
103+
104+
try (OutputStream os = connection.getOutputStream()) {
105+
os.write(requestBody.getPartDate());
106+
}
107+
108+
if (connection.getResponseCode() != 200) {
109+
String error = readStream(connection.getErrorStream());
110+
throw new RuntimeException("Failed to upload part " + request.getPartNumber() + ": " + connection.getResponseCode() + " " + error);
111+
}
112+
String eTag = connection.getHeaderField("ETag");
113+
return new UploadPartResponse(eTag);
114+
}
115+
116+
public CompleteMultipartResponse completeMultipartUpload(CompleteMultipartRequest request)
117+
throws NoSuchAlgorithmException {
118+
String resourcePath = "/" + BUCKET_NAME + "/" + OBJECT_NAME;
119+
String queryString = "?uploadId=" + request.getUploadId();
120+
String uri = GCS_ENDPOINT + resourcePath + queryString;
121+
122+
StringBuilder xmlBodyBuilder = new StringBuilder("<CompleteMultipartUpload>\n");
123+
for (CompletedPart part : request.getCompletedParts()) {
124+
xmlBodyBuilder.append(" <Part>\n");
125+
xmlBodyBuilder.append(" <PartNumber>").append(part.getPartNumber()).append("</PartNumber>\n");
126+
xmlBodyBuilder.append(" <ETag>").append(part.getEtag()).append("</ETag>\n");
127+
xmlBodyBuilder.append(" </Part>\n");
128+
}
129+
xmlBodyBuilder.append("</CompleteMultipartUpload>");
130+
byte[] xmlBodyBytes = xmlBodyBuilder.toString().getBytes(StandardCharsets.UTF_8);
131+
132+
MessageDigest md = MessageDigest.getInstance("MD5");
133+
String contentMd5 = Base64.getEncoder().encodeToString(md.digest(xmlBodyBytes));
134+
String date = getRfc1123Date();
135+
String contentType = "application/xml";
136+
137+
// GCS Signature Rule #3: The query string IS NOT included for the POST complete request.
138+
String signature = signRequest("POST", contentMd5, contentType, date, resourcePath);
139+
String authHeader = "GOOG1 " + GOOGLE_ACCESS_KEY + ":" + signature;
140+
141+
HttpURLConnection connection = (HttpURLConnection) new URL(uri).openConnection();
142+
connection.setRequestMethod("POST");
143+
connection.setRequestProperty("Date", date);
144+
connection.setRequestProperty("Authorization", authHeader);
145+
connection.setRequestProperty("Content-Type", contentType);
146+
connection.setRequestProperty("Content-MD5", contentMd5);
147+
connection.setFixedLengthStreamingMode(xmlBodyBytes.length);
148+
connection.setDoOutput(true);
149+
150+
try (OutputStream os = connection.getOutputStream()) {
151+
os.write(xmlBodyBytes);
152+
}
153+
154+
if (connection.getResponseCode() != 200) {
155+
String error = readStream(connection.getErrorStream());
156+
throw new RuntimeException("Failed to complete upload: " + connection.getResponseCode() + " " + error);
157+
}
158+
return null;
159+
}
160+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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.multipartuploader;
17+
18+
import java.io.BufferedReader;
19+
import java.io.File;
20+
import java.io.FileInputStream;
21+
import java.io.IOException;
22+
import java.io.InputStream;
23+
import java.io.InputStreamReader;
24+
import java.nio.charset.StandardCharsets;
25+
import java.time.ZoneId;
26+
import java.time.ZonedDateTime;
27+
import java.time.format.DateTimeFormatter;
28+
import java.util.Base64;
29+
import javax.crypto.Mac;
30+
import javax.crypto.spec.SecretKeySpec;
31+
32+
public class MultipartUploaderUtility {
33+
public static String readStream(InputStream inputStream) throws IOException {
34+
if (inputStream == null) return "";
35+
StringBuilder response = new StringBuilder();
36+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
37+
String line;
38+
while ((line = reader.readLine()) != null) {
39+
response.append(line);
40+
}
41+
}
42+
return response.toString();
43+
}
44+
45+
public static String signRequest(String httpVerb, String contentMd5, String contentType, String date, String canonicalizedResource, String googleSecretKey) {
46+
try {
47+
String stringToSign = httpVerb + "\n" + contentMd5 + "\n" + contentType + "\n" + date + "\n" + canonicalizedResource;
48+
Mac sha1Hmac = Mac.getInstance("HmacSHA1");
49+
SecretKeySpec secretKey = new SecretKeySpec(googleSecretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA1");
50+
sha1Hmac.init(secretKey);
51+
byte[] signatureBytes = sha1Hmac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
52+
return Base64.getEncoder().encodeToString(signatureBytes);
53+
} catch (Exception e) {
54+
throw new RuntimeException("Failed to sign request", e);
55+
}
56+
}
57+
58+
public static byte[] readPart(File file, long position, int size) throws IOException {
59+
byte[] buffer = new byte[size];
60+
try (FileInputStream fis = new FileInputStream(file)) {
61+
fis.getChannel().position(position);
62+
int bytesRead = fis.read(buffer, 0, size);
63+
if (bytesRead != size) {
64+
byte[] smallerBuffer = new byte[bytesRead];
65+
System.arraycopy(buffer, 0, smallerBuffer, 0, bytesRead);
66+
return smallerBuffer;
67+
}
68+
}
69+
return buffer;
70+
}
71+
72+
public static String getRfc1123Date() {
73+
return DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneId.of("GMT")).format(ZonedDateTime.now());
74+
}
75+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
17+
package com.google.cloud.storage.multipartuploader.data;
18+
19+
public class CompleteMultipartRequest {
20+
21+
public String getUploadId() {
22+
}
23+
24+
public CompletedPart[] getCompletedParts() {
25+
}
26+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
17+
package com.google.cloud.storage.multipartuploader.data;
18+
19+
public class CompleteMultipartResponse {
20+
21+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.google.cloud.storage.multipartuploader.data;
2+
3+
public class CompletedPart {
4+
5+
private final int partNumber;
6+
7+
private final String etag;
8+
9+
private CompletedPart(int partNumber, String etag) {
10+
this.partNumber = partNumber;
11+
this.etag = etag;
12+
}
13+
14+
public static Builder newBuilder() {
15+
return new Builder();
16+
}
17+
18+
public int getPartNumber() {
19+
return partNumber;
20+
}
21+
22+
public String getEtag() {
23+
return etag;
24+
}
25+
26+
public static class Builder {
27+
private int partNumber;
28+
private String etag;
29+
30+
public Builder setPartNumber(int partNumber) {
31+
this.partNumber = partNumber;
32+
return this;
33+
}
34+
35+
public Builder setEtag(String etag) {
36+
this.etag = etag;
37+
return this;
38+
}
39+
40+
public CompletedPart build() {
41+
return new CompletedPart(partNumber, etag);
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)