Skip to content

Commit 1f3d34a

Browse files
authored
Google Cloud Storage API [WIP] (#47)
* Adding the GCS API * Fixing dependency declaration * Integration tests for GCS API * Updating the namespace decl * Cleaning up GCS integration test * Cleaned up integration test * Deferring the import to GCS, and custom handling the import error * Removing the check for non existing buckets * Throwing an error if the credential is not supported * Fixing some lint errors * Adding GCS lib as a dependency * Adding GCS lib as a dependency * Added GCS typescript definitions * Adding a new test case; Fixing some typos; Other minor improvements
1 parent 9a389a6 commit 1f3d34a

File tree

14 files changed

+4174
-0
lines changed

14 files changed

+4174
-0
lines changed

npm-shrinkwrap.json

Lines changed: 3740 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
"del": "^2.2.1",
4848
"firebase": "^3.6.9",
4949
"firebase-token-generator": "^2.0.0",
50+
"@google-cloud/storage": "^1.2.1",
51+
"@types/google-cloud__storage": "^1.1.1",
5052
"gulp": "^3.9.1",
5153
"gulp-exit": "0.0.2",
5254
"gulp-header": "^1.8.8",

src/firebase-app.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,14 @@ export class FirebaseApp {
302302
);
303303
}
304304

305+
/* istanbul ignore next */
306+
public storage(): FirebaseServiceInterface {
307+
throw new FirebaseAppError(
308+
AppErrorCodes.INTERNAL_ERROR,
309+
'INTERNAL ASSERT FAILED: Firebase storage() service has not been registered.',
310+
);
311+
}
312+
305313
/**
306314
* Returns the name of the FirebaseApp instance.
307315
*

src/firebase-namespace.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,14 @@ export class FirebaseNamespace {
300300
);
301301
}
302302

303+
/* istanbul ignore next */
304+
public storage(): FirebaseServiceFactory {
305+
throw new FirebaseAppError(
306+
AppErrorCodes.INTERNAL_ERROR,
307+
'INTERNAL ASSERT FAILED: Firebase storage() service has not been registered.',
308+
);
309+
}
310+
303311
/**
304312
* Initializes the FirebaseApp instance.
305313
*

src/index.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17+
import {Bucket} from '@google-cloud/storage';
18+
1719
declare namespace admin {
1820
interface FirebaseError {
1921
code: string;
@@ -53,6 +55,7 @@ declare namespace admin {
5355
function auth(app?: admin.app.App): admin.auth.Auth;
5456
function database(app?: admin.app.App): admin.database.Database;
5557
function messaging(app?: admin.app.App): admin.messaging.Messaging;
58+
function storage(app?: admin.app.App): admin.storage.Storage;
5659
function initializeApp(options: admin.AppOptions, name?: string): admin.app.App;
5760
}
5861

@@ -64,6 +67,7 @@ declare namespace admin.app {
6467
auth(): admin.auth.Auth;
6568
database(): admin.database.Database;
6669
messaging(): admin.messaging.Messaging;
70+
storage(): admin.storage.Storage;
6771
delete(): Promise<void>;
6872
}
6973
}
@@ -381,6 +385,13 @@ declare namespace admin.messaging {
381385
}
382386
}
383387

388+
declare namespace admin.storage {
389+
interface Storage {
390+
app: admin.app.App;
391+
bucket(name?: string): Bucket;
392+
}
393+
}
394+
384395
declare module 'firebase-admin' {
385396
export = admin;
386397
}

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import * as firebase from './default-namespace';
1818
import registerAuth from './auth/register-auth';
1919
import registerMessaging from './messaging/register-messaging';
20+
import registerStorage from './storage/register-storage';
2021

2122
// Register the Database service
2223
// For historical reasons, the database code is included as minified code and registers itself
@@ -31,4 +32,7 @@ registerAuth();
3132
// Register the Messaging service
3233
registerMessaging();
3334

35+
// Register the Storage service
36+
registerStorage();
37+
3438
export = firebase;

src/storage/register-storage.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {Storage} from './storage';
2+
import {AppHook, FirebaseApp} from '../firebase-app';
3+
import {FirebaseServiceInterface} from '../firebase-service';
4+
import * as firebase from '../default-namespace';
5+
import {FirebaseServiceNamespace} from '../firebase-namespace';
6+
7+
/**
8+
* Factory function that creates a new Storage service.
9+
*
10+
* @param {Object} app The app for this service.
11+
* @param {function(Object)} extendApp An extend function to extend the app namespace.
12+
*
13+
* @return {Storage} The Storage service for the specified app.
14+
*/
15+
function serviceFactory(app: FirebaseApp, extendApp: (props: Object) => void): FirebaseServiceInterface {
16+
return new Storage(app);
17+
}
18+
19+
/**
20+
* Handles app life-cycle events.
21+
*
22+
* @param {string} event The app event that is occurring.
23+
* @param {FirebaseApp} app The app for which the app hook is firing.
24+
*/
25+
let appHook: AppHook = (event: string, app: FirebaseApp) => {
26+
return;
27+
};
28+
29+
export default function(): FirebaseServiceNamespace<FirebaseServiceInterface> {
30+
return firebase.INTERNAL.registerService(
31+
'storage',
32+
serviceFactory,
33+
{Storage},
34+
appHook
35+
);
36+
}

src/storage/storage.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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+
import {FirebaseApp} from '../firebase-app';
18+
import {FirebaseError} from '../utils/error';
19+
import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service';
20+
import {ApplicationDefaultCredential} from '../auth/credential';
21+
import {Bucket} from '@google-cloud/storage';
22+
23+
import * as validator from '../utils/validator';
24+
25+
/**
26+
* Internals of a Storage instance.
27+
*/
28+
class StorageInternals implements FirebaseServiceInternalsInterface {
29+
/**
30+
* Deletes the service and its associated resources.
31+
*
32+
* @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted.
33+
*/
34+
public delete(): Promise<void> {
35+
// There are no resources to clean up.
36+
return Promise.resolve();
37+
}
38+
}
39+
40+
/**
41+
* Storage service bound to the provided app.
42+
*/
43+
export class Storage implements FirebaseServiceInterface {
44+
public INTERNAL: StorageInternals = new StorageInternals();
45+
46+
private appInternal: FirebaseApp;
47+
private storageClient: any;
48+
49+
/**
50+
* @param {Object} app The app for this Storage service.
51+
* @constructor
52+
*/
53+
constructor(app: FirebaseApp) {
54+
if (!validator.isNonNullObject(app) || !('options' in app)) {
55+
throw new FirebaseError({
56+
code: 'storage/invalid-argument',
57+
message: 'First argument passed to admin.storage() must be a valid Firebase app instance.',
58+
});
59+
}
60+
61+
let storage;
62+
try {
63+
/* tslint:disable-next-line:no-var-requires */
64+
storage = require('@google-cloud/storage');
65+
} catch (e) {
66+
throw new FirebaseError({
67+
code: 'storage/missing-dependencies',
68+
message: 'Failed to import the Cloud Storage client library for Node.js. '
69+
+ 'Make sure to install the "@google-cloud/storage" npm package.',
70+
});
71+
}
72+
73+
const cert = app.options.credential.getCertificate();
74+
if (cert != null) {
75+
// cert is available when the SDK has been initialized with a service account JSON file,
76+
// or by setting the GOOGLE_APPLICATION_CREDENTIALS envrionment variable.
77+
this.storageClient = storage({
78+
credentials: {
79+
private_key: cert.privateKey,
80+
client_email: cert.clientEmail,
81+
},
82+
});
83+
} else if (app.options.credential instanceof ApplicationDefaultCredential) {
84+
// Try to use the Google application default credentials.
85+
this.storageClient = storage();
86+
} else {
87+
throw new FirebaseError({
88+
code: 'storage/invalid-credential',
89+
message: 'Failed to initialize Google Cloud Storage client with the available credential. ' +
90+
'Must initialize the SDK with a certificate credential or application default credentials ' +
91+
'to use Cloud Storage API.',
92+
});
93+
}
94+
this.appInternal = app;
95+
}
96+
97+
public bucket(name?: string): Bucket {
98+
let bucketName;
99+
if (typeof name !== 'undefined') {
100+
bucketName = name;
101+
} else {
102+
bucketName = this.appInternal.options.storageBucket;
103+
}
104+
105+
if (validator.isNonEmptyString(bucketName)) {
106+
return this.storageClient.bucket(bucketName);
107+
}
108+
throw new FirebaseError({
109+
code: 'storage/invalid-argument',
110+
message: 'Bucket name not specified or invalid. Specify a valid bucket name via the ' +
111+
'storageBucket option when initializing the app, or specify the bucket name ' +
112+
'explicitly when calling the getBucket() method.',
113+
});
114+
}
115+
116+
/**
117+
* Returns the app associated with this Storage instance.
118+
*
119+
* @return {FirebaseApp} The app associated with this Storage instance.
120+
*/
121+
get app(): FirebaseApp {
122+
return this.appInternal;
123+
}
124+
};

test/integration/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,26 @@ var app = require('./app');
3939
var auth = require('./auth');
4040
var database = require('./database');
4141
var messaging = require('./messaging');
42+
var storage = require('./storage');
4243

4344
var apiRequest = require('../../lib/utils/api-request');
4445
var url = require('url');
4546

4647
var serviceAccount = utils.getCredential();
4748
var databaseURL = 'https://' + utils.getProjectId() + '.firebaseio.com';
49+
var storageBucket = utils.getProjectId() + '.appspot.com';
4850

4951
var defaultApp = admin.initializeApp({
5052
credential: admin.credential.cert(serviceAccount),
5153
databaseURL: databaseURL,
54+
storageBucket: storageBucket,
5255
});
5356

5457
var nullApp = admin.initializeApp({
5558
credential: admin.credential.cert(serviceAccount),
5659
databaseURL: databaseURL,
5760
databaseAuthVariableOverride: null,
61+
storageBucket: storageBucket,
5862
}, 'null');
5963

6064
var nonNullApp = admin.initializeApp({
@@ -63,6 +67,7 @@ var nonNullApp = admin.initializeApp({
6367
databaseAuthVariableOverride: {
6468
uid: utils.generateRandomString(20),
6569
},
70+
storageBucket: storageBucket,
6671
}, 'nonNull');
6772

6873

@@ -206,6 +211,7 @@ return promptForUpdateRules(flags['overwrite'])
206211
.then(_.partial(auth.test, utils))
207212
.then(_.partial(database.test, utils))
208213
.then(_.partial(messaging.test, utils))
214+
.then(_.partial(storage.test, utils))
209215
.then(utils.logResults)
210216
.catch(function(error) {
211217
console.log(chalk.red('\nSOMETHING WENT WRONG!', error));

test/integration/storage.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
var admin = require('../../lib/index');
18+
var stream = require('stream');
19+
20+
function test(utils) {
21+
console.log('\nStorage:');
22+
23+
function testDefaultBucket() {
24+
const bucket = admin.storage().bucket();
25+
return verifyBucket(bucket, 'storage().bucket()')
26+
.then(() => {
27+
utils.logSuccess('storage().bucket()');
28+
})
29+
.catch((error) => {
30+
handleError(error, 'storage().bucket()');
31+
});
32+
}
33+
34+
function testCustomBucket() {
35+
const bucket = admin.storage().bucket(utils.getProjectId() + '.appspot.com');
36+
return verifyBucket(bucket, 'storage().bucket(string)')
37+
.then(() => {
38+
utils.logSuccess('storage().bucket(string)');
39+
})
40+
.catch((error) => {
41+
handleError(error, 'storage().bucket(string)');
42+
});
43+
}
44+
45+
function testNonExistingBucket() {
46+
const bucket = admin.storage().bucket('non.existing');
47+
return bucket.exists()
48+
.then((data) => {
49+
utils.assert(!data[0], 'storage().bucket("non.existing").exists() returned true');
50+
})
51+
.catch((error) => {
52+
handleError(error, 'storage().bucket("non.existing")');
53+
});
54+
}
55+
56+
function verifyBucket(bucket, testName) {
57+
const expected = 'Hello World: ' + testName;
58+
const file = bucket.file('data_' + Date.now() + '.txt');
59+
return file.save(expected)
60+
.then(() => {
61+
return file.download();
62+
})
63+
.then((data) => {
64+
if (data[0].toString() != expected) {
65+
return Promise.reject('Data read from GCS does not match expected');
66+
}
67+
return file.delete();
68+
})
69+
.then((resp) => {
70+
return file.exists();
71+
})
72+
.then((data) => {
73+
if (data[0]) {
74+
return Promise.reject('Failed to delete file from GCS');
75+
}
76+
});
77+
}
78+
79+
function handleError(error, testName) {
80+
let reason;
81+
if (error.message) {
82+
reason = error.message;
83+
} else {
84+
reason = JSON.stringify(error);
85+
}
86+
utils.logFailure(
87+
testName,
88+
'Error while interacting with bucket: ' + reason);
89+
}
90+
91+
return Promise.resolve()
92+
.then(testDefaultBucket)
93+
.then(testCustomBucket)
94+
.then(testNonExistingBucket);
95+
}
96+
97+
module.exports = {
98+
test: test
99+
}

0 commit comments

Comments
 (0)