Skip to content

Commit a438e50

Browse files
authored
Google Cloud Storage API [WIP] (firebase#51)
* Prototype GCS integration * Refactored cloud storage API wrapper * Updated comment * Throwing an exception for non-existing buckets * Renamed StorageWrapper to StorageClient * Updated options test * Implementing a getDownloadUrl() method for server-side * Making the dependency required * Updated comments/javadocs; Renamed FirebaseOAuthCredentials to FirebaseCloudCredentials; URL escaping download tokens when generating download URLs for GCS objects * URL escaping object names in generated URLs * Renaming getBucket() to bucket() as per API review recommendations * Removed getDownloadUrl() method as per API review recommendation * Making StorageClient initialization thread safe
1 parent 9779637 commit a438e50

9 files changed

Lines changed: 419 additions & 8 deletions

File tree

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,11 @@
329329
<artifactId>guava</artifactId>
330330
<version>20.0</version>
331331
</dependency>
332+
<dependency>
333+
<groupId>com.google.cloud</groupId>
334+
<artifactId>google-cloud-storage</artifactId>
335+
<version>1.2.1</version>
336+
</dependency>
332337
<dependency>
333338
<groupId>org.slf4j</groupId>
334339
<artifactId>slf4j-api</artifactId>

src/main/java/com/google/firebase/FirebaseOptions.java

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616

1717
package com.google.firebase;
1818

19+
import static com.google.common.base.Preconditions.checkArgument;
1920
import static com.google.common.base.Preconditions.checkNotNull;
2021

2122
import com.google.api.client.googleapis.util.Utils;
2223
import com.google.api.client.http.HttpTransport;
2324
import com.google.api.client.json.JsonFactory;
24-
import com.google.common.base.MoreObjects;
25+
import com.google.common.base.Strings;
2526
import com.google.firebase.auth.FirebaseCredential;
2627
import com.google.firebase.auth.FirebaseCredentials;
2728
import com.google.firebase.internal.NonNull;
@@ -36,6 +37,7 @@ public final class FirebaseOptions {
3637
// TODO: deprecate and remove it once we can fetch these from Remote Config.
3738

3839
private final String databaseUrl;
40+
private final String storageBucket;
3941
private final FirebaseCredential firebaseCredential;
4042
private final Map<String, Object> databaseAuthVariableOverride;
4143
private final HttpTransport httpTransport;
@@ -46,6 +48,7 @@ private FirebaseOptions(@NonNull FirebaseOptions.Builder builder) {
4648
"FirebaseOptions must be initialized with setCredential().");
4749
this.databaseUrl = builder.databaseUrl;
4850
this.databaseAuthVariableOverride = builder.databaseAuthVariableOverride;
51+
this.storageBucket = builder.storageBucket;
4952
this.httpTransport = checkNotNull(builder.httpTransport,
5053
"FirebaseOptions must be initialized with a non-null HttpTransport.");
5154
this.jsonFactory = checkNotNull(builder.jsonFactory,
@@ -61,6 +64,15 @@ public String getDatabaseUrl() {
6164
return databaseUrl;
6265
}
6366

67+
/**
68+
* Returns the name of the Google Cloud Storage bucket used for storing application data.
69+
*
70+
* @return The cloud storage bucket name set via {@link Builder#setStorageBucket}
71+
*/
72+
public String getStorageBucket() {
73+
return storageBucket;
74+
}
75+
6476
FirebaseCredential getCredential() {
6577
return firebaseCredential;
6678
}
@@ -102,6 +114,7 @@ public JsonFactory getJsonFactory() {
102114
public static final class Builder {
103115

104116
private String databaseUrl;
117+
private String storageBucket;
105118
private FirebaseCredential firebaseCredential;
106119
private Map<String, Object> databaseAuthVariableOverride = new HashMap<>();
107120
private HttpTransport httpTransport = Utils.getDefaultTransport();
@@ -118,6 +131,7 @@ public Builder() {}
118131
*/
119132
public Builder(FirebaseOptions options) {
120133
databaseUrl = options.databaseUrl;
134+
storageBucket = options.storageBucket;
121135
firebaseCredential = options.firebaseCredential;
122136
databaseAuthVariableOverride = options.databaseAuthVariableOverride;
123137
httpTransport = options.httpTransport;
@@ -127,8 +141,8 @@ public Builder(FirebaseOptions options) {
127141
/**
128142
* Sets the Realtime Database URL to use for data storage.
129143
*
130-
* <p>See <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2F%3Cspan+class%3D"x x-first x-last">/docs/admin/setup#initialize_the_sdk">Initialize the SDK</a> for code samples
131-
* and detailed documentation.
144+
* <p>See <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2F%3Cspan+class%3D"x x-first x-last">https://firebase.google.com/docs/admin/setup#initialize_the_sdk">
145+
* Initialize the SDK</a> for code samples and detailed documentation.
132146
*
133147
* @param databaseUrl The Realtime Database URL to use for data storage.
134148
* @return This <code>Builder</code> instance is returned so subsequent calls can be chained.
@@ -138,11 +152,29 @@ public Builder setDatabaseUrl(@Nullable String databaseUrl) {
138152
return this;
139153
}
140154

155+
/**
156+
* Sets the name of the Google Cloud Storage bucket for reading and writing application data.
157+
* The same credential used to initialize the SDK (see {@link Builder#setCredential}) will be
158+
* used to access the bucket.
159+
*
160+
* <p>See <a href="https://firebase.google.com/docs/admin/setup#initialize_the_sdk">
161+
* Initialize the SDK</a> for code samples and detailed documentation.
162+
*
163+
* @param storageBucket The name of an existing Google Cloud Storage bucket.
164+
* @return This <code>Builder</code> instance is returned so subsequent calls can be chained.
165+
*/
166+
public Builder setStorageBucket(String storageBucket) {
167+
checkArgument(!Strings.isNullOrEmpty(storageBucket),
168+
"Storage bucket must not be null or empty");
169+
this.storageBucket = storageBucket;
170+
return this;
171+
}
172+
141173
/**
142174
* Sets the <code>FirebaseCredential</code> to use to authenticate the SDK.
143175
*
144-
* <p>See <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2F%3Cspan+class%3D"x x-first x-last">/docs/admin/setup#initialize_the_sdk">Initialize the SDK</a> for code samples
145-
* and detailed documentation.
176+
* <p>See <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2F%3Cspan+class%3D"x x-first x-last">https://firebase.google.com/docs/admin/setup#initialize_the_sdk">
177+
* Initialize the SDK</a> for code samples and detailed documentation.
146178
*
147179
* @param credential A <code>FirebaseCredential</code> used to authenticate the SDK. See {@link
148180
* FirebaseCredentials} for default implementations.
@@ -164,8 +196,8 @@ public Builder setCredential(@NonNull FirebaseCredential credential) {
164196
* instance. If this option is set to <code>null</code>, security rules are evaluated against an
165197
* unauthenticated user. That is, the <code>auth</code> variable is <code>null</code>.
166198
*
167-
* <p>See <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2F%3Cspan+class%3D"x x-first x-last">/docs/database/admin/start#authenticate-with-limited-privileges">Authenticate
168-
* with limited privileges</a> for code samples and detailed documentation.
199+
* <p>See <a href="proxy.php?url=https%3A%2F%2Fgithub.com%2F%3Cspan+class%3D"x x-first x-last">https://firebase.google.com/docs/database/admin/start#authenticate-with-limited-privileges">
200+
* Authenticate with limited privileges</a> for code samples and detailed documentation.
169201
*
170202
* @param databaseAuthVariableOverride The value to use for the <code>auth</code> variable in
171203
* the security rules for Realtime Database actions.

src/main/java/com/google/firebase/auth/FirebaseCredentials.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,17 @@ public class FirebaseCredentials {
4848

4949
private static final List<String> FIREBASE_SCOPES =
5050
ImmutableList.of(
51+
// Enables access to Firebase Realtime Database.
5152
"https://www.googleapis.com/auth/firebase.database",
53+
54+
// Enables access to the email address associated with a project.
5255
"https://www.googleapis.com/auth/userinfo.email",
53-
"https://www.googleapis.com/auth/identitytoolkit");
56+
57+
// Enables access to Google Identity Toolkit (for user management APIs).
58+
"https://www.googleapis.com/auth/identitytoolkit",
59+
60+
// Enables access to Google Cloud Storage.
61+
"https://www.googleapis.com/auth/devstorage.full_control");
5462

5563
private FirebaseCredentials() {
5664
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright 2017 Google Inc.
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.firebase.cloud;
18+
19+
import static com.google.common.base.Preconditions.checkArgument;
20+
import static com.google.common.base.Preconditions.checkNotNull;
21+
22+
import com.google.cloud.storage.Bucket;
23+
import com.google.cloud.storage.Storage;
24+
import com.google.cloud.storage.StorageOptions;
25+
import com.google.common.base.Strings;
26+
import com.google.firebase.FirebaseApp;
27+
import com.google.firebase.ImplFirebaseTrampolines;
28+
import com.google.firebase.internal.FirebaseCloudCredentials;
29+
import com.google.firebase.internal.FirebaseService;
30+
31+
/**
32+
* StorageClient provides access to Google Cloud Storage APIs. You can specify a default cloud
33+
* storage bucket via {@link com.google.firebase.FirebaseOptions}, and then get a reference to this
34+
* default bucket by calling {@link StorageClient#bucket()}. Or you can get a reference to a
35+
* specific bucket at any time by calling {@link StorageClient#bucket(String)}.
36+
*
37+
* <p>This class requires Google Cloud Storage libraries for Java. Make sure the artifact
38+
* google-cloud-storage is in the classpath along with its transitive dependencies.
39+
*/
40+
public class StorageClient {
41+
42+
private final FirebaseApp app;
43+
private final Storage storage;
44+
45+
private StorageClient(FirebaseApp app) {
46+
this.app = checkNotNull(app, "FirebaseApp must not be null");
47+
this.storage = StorageOptions.newBuilder()
48+
.setCredentials(new FirebaseCloudCredentials(app))
49+
.build()
50+
.getService();
51+
}
52+
53+
public static StorageClient getInstance() {
54+
return getInstance(FirebaseApp.getInstance());
55+
}
56+
57+
public static synchronized StorageClient getInstance(FirebaseApp app) {
58+
StorageClientService service = ImplFirebaseTrampolines.getService(app, SERVICE_ID,
59+
StorageClientService.class);
60+
if (service == null) {
61+
service = ImplFirebaseTrampolines.addService(app, new StorageClientService(app));
62+
}
63+
return service.getInstance();
64+
}
65+
66+
/**
67+
* Returns the default cloud storage bucket associated with the current app. This is the bucket
68+
* configured via {@link com.google.firebase.FirebaseOptions} when initializing the app. If
69+
* no bucket was configured via options, this method throws an exception.
70+
*
71+
* @return a cloud storage Bucket instance, or null if the configured bucket does not exist.
72+
* @throws IllegalArgumentException If no bucket is configured via <code>FirebaseOptions</code>,
73+
* or if the bucket does not exist.
74+
*/
75+
public Bucket bucket() {
76+
return bucket(app.getOptions().getStorageBucket());
77+
}
78+
79+
/**
80+
* Returns a cloud storage Bucket instance for the specified bucket name.
81+
*
82+
* @param name a non-null, non-empty bucket name.
83+
* @return a cloud storage Bucket instance, or null if the specified bucket does not exist.
84+
* @throws IllegalArgumentException If the bucket name is null, empty, or if the specified
85+
* bucket does not exist.
86+
*/
87+
public Bucket bucket(String name) {
88+
checkArgument(!Strings.isNullOrEmpty(name),
89+
"Bucket name not specified. Specify the bucket name via the storageBucket "
90+
+ "option when initializing the app, or specify the bucket name explicitly when "
91+
+ "calling the getBucket() method.");
92+
Bucket bucket = storage.get(name);
93+
checkArgument(bucket != null, "Bucket " + name + " does not exist.");
94+
return bucket;
95+
}
96+
97+
private static final String SERVICE_ID = StorageClient.class.getName();
98+
99+
private static class StorageClientService extends FirebaseService<StorageClient> {
100+
101+
StorageClientService(FirebaseApp app) {
102+
super(SERVICE_ID, new StorageClient(app));
103+
}
104+
105+
@Override
106+
public void destroy() {
107+
// NOTE: We don't explicitly tear down anything here, but public methods of StorageClient
108+
// will now fail because calls to getOptions() and getToken() will hit FirebaseApp,
109+
// which will throw once the app is deleted.
110+
}
111+
}
112+
113+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2017 Google Inc.
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.firebase.internal;
18+
19+
import static com.google.common.base.Preconditions.checkNotNull;
20+
21+
import com.google.auth.Credentials;
22+
import com.google.common.collect.ImmutableList;
23+
import com.google.common.collect.ImmutableMap;
24+
import com.google.firebase.FirebaseApp;
25+
import com.google.firebase.ImplFirebaseTrampolines;
26+
import com.google.firebase.tasks.Continuation;
27+
import com.google.firebase.tasks.Task;
28+
import com.google.firebase.tasks.Tasks;
29+
import java.io.IOException;
30+
import java.net.URI;
31+
import java.util.List;
32+
import java.util.Map;
33+
import java.util.concurrent.ExecutionException;
34+
35+
/**
36+
* A Google cloud credential implementation that uses OAuth2 access tokens obtained from a
37+
* <code>FirebaseApp</code> to authenticate cloud API calls. This essentially acts as a bridge
38+
* between Firebase Admin SDK APIs and the Google cloud <code>Credentials</code> API.
39+
*/
40+
public final class FirebaseCloudCredentials extends Credentials {
41+
42+
private final FirebaseApp app;
43+
44+
public FirebaseCloudCredentials(FirebaseApp app) {
45+
this.app = checkNotNull(app);
46+
}
47+
48+
@Override
49+
public String getAuthenticationType() {
50+
return "OAuth2";
51+
}
52+
53+
@Override
54+
public Map<String, List<String>> getRequestMetadata(URI uri) throws IOException {
55+
Task<String> task = ImplFirebaseTrampolines.getToken(app, false).continueWith(
56+
new Continuation<GetTokenResult, String>() {
57+
@Override
58+
public String then(Task<GetTokenResult> task) throws Exception {
59+
return task.getResult().getToken();
60+
}
61+
});
62+
try {
63+
String authHeader = "Bearer " + Tasks.await(task);
64+
return ImmutableMap.<String, List<String>>of(
65+
"Authorization", ImmutableList.of(authHeader));
66+
} catch (ExecutionException | InterruptedException e) {
67+
throw new IOException("Failed to acquire an OAuth token", e);
68+
}
69+
}
70+
71+
@Override
72+
public boolean hasRequestMetadata() {
73+
return true;
74+
}
75+
76+
@Override
77+
public boolean hasRequestMetadataOnly() {
78+
return true;
79+
}
80+
81+
@Override
82+
public void refresh() throws IOException {
83+
ImplFirebaseTrampolines.getToken(app, true);
84+
}
85+
86+
}

src/test/java/com/google/firebase/FirebaseOptionsTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.junit.Assert.assertFalse;
2121
import static org.junit.Assert.assertNotNull;
2222
import static org.junit.Assert.assertNotSame;
23+
import static org.junit.Assert.assertNull;
2324
import static org.junit.Assert.assertSame;
2425
import static org.junit.Assert.fail;
2526

@@ -45,6 +46,7 @@
4546
public class FirebaseOptionsTest {
4647

4748
private static final String FIREBASE_DB_URL = "https://mock-project.firebaseio.com";
49+
private static final String FIREBASE_STORAGE_BUCKET = "mock-storage-bucket";
4850

4951
private static final FirebaseOptions ALL_VALUES_OPTIONS =
5052
new FirebaseOptions.Builder()
@@ -60,11 +62,13 @@ public void createOptionsWithAllValuesSet() throws IOException, InterruptedExcep
6062
FirebaseOptions firebaseOptions =
6163
new FirebaseOptions.Builder()
6264
.setDatabaseUrl(FIREBASE_DB_URL)
65+
.setStorageBucket(FIREBASE_STORAGE_BUCKET)
6366
.setCredential(FirebaseCredentials.fromCertificate(ServiceAccount.EDITOR.asStream()))
6467
.setJsonFactory(jsonFactory)
6568
.setHttpTransport(httpTransport)
6669
.build();
6770
assertEquals(FIREBASE_DB_URL, firebaseOptions.getDatabaseUrl());
71+
assertEquals(FIREBASE_STORAGE_BUCKET, firebaseOptions.getStorageBucket());
6872
assertSame(jsonFactory, firebaseOptions.getJsonFactory());
6973
assertSame(httpTransport, firebaseOptions.getHttpTransport());
7074
TestOnlyImplFirebaseAuthTrampolines.getCertificate(firebaseOptions.getCredential())
@@ -89,6 +93,8 @@ public void createOptionsWithOnlyMandatoryValuesSet() throws IOException, Interr
8993
.build();
9094
assertNotNull(firebaseOptions.getJsonFactory());
9195
assertNotNull(firebaseOptions.getHttpTransport());
96+
assertNull(firebaseOptions.getDatabaseUrl());
97+
assertNull(firebaseOptions.getStorageBucket());
9298
TestOnlyImplFirebaseAuthTrampolines.getCertificate(firebaseOptions.getCredential())
9399
.addOnSuccessListener(
94100
new OnSuccessListener<GoogleCredential>() {

0 commit comments

Comments
 (0)