Skip to content
1 change: 1 addition & 0 deletions firebase_admin/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

_request = requests.Request()
_scopes = [
'https://www.googleapis.com/auth/devstorage.read_write',
'https://www.googleapis.com/auth/firebase',
'https://www.googleapis.com/auth/userinfo.email'
]
Expand Down
88 changes: 88 additions & 0 deletions firebase_admin/storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Copyright 2017 Google Inc.
#
# 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.

"""Firebase Cloud Storage module.

This module contains utilities for accessing Google Cloud Storage buckets associated with
Firebase apps. This requires installing the google-cloud-storage Python module separately.
"""

# pylint: disable=import-error,no-name-in-module
try:
from google.cloud import storage
except ImportError:
raise ImportError('Failed to import the Cloud Storage library for Python. Make sure '
'to install the "google-cloud-storage" module.')

import six

from firebase_admin import utils


_STORAGE_ATTRIBUTE = '_storage'

def bucket(name=None, app=None):
"""Returns a handle to a Google Cloud Storage bucket.

If the name argument is not provided, uses the 'storageBucket' option specified when
initializing the App. If that is also not available raises an error. This function
does not make any RPC calls.

Args:
name: Name of a cloud storage bucket (optional).
app: An App instance (optional).

Returns:
google.cloud.storage.Bucket: A handle to the specified bucket.

Raises:
ValueError: If a bucket name is not specified either via options or method arguments,
or if the specified bucket name is not a valid string.
"""
client = utils.get_app_service(app, _STORAGE_ATTRIBUTE, _StorageClient.from_app)
return client.bucket(name)


class _StorageClient(object):
"""Holds a Google Cloud Storage client instance."""

def __init__(self, credentials, project, default_bucket):
self._client = storage.Client(credentials=credentials, project=project)
self._default_bucket = default_bucket

@classmethod
def from_app(cls, app):
credentials = app.credential.get_credential()
# Specifying project ID is not required, but providing it when available
# significantly speeds up the initialization of the storage client.
try:
project = app.credential.project_id
except AttributeError:
project = None
default_bucket = app.options.get('storageBucket')
return _StorageClient(credentials, project, default_bucket)

def bucket(self, name=None):
"""Returns a handle to the specified Cloud Storage Bucket."""
bucket_name = name if name is not None else self._default_bucket
if bucket_name is None:
raise ValueError(
'Storage bucket name not specified. Specify the bucket name via the '
'"storageBucket" option when initializing the App, or specify the bucket '
'name explicitly when calling the storage.bucket() function.')
elif not bucket_name or not isinstance(bucket_name, six.string_types):
raise ValueError(
'Invalid storage bucket name: "{0}". Bucket name must be a non-empty '
'string.'.format(bucket_name))
return self._client.bucket(bucket_name)
10 changes: 9 additions & 1 deletion integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ def integration_conf(request):
raise ValueError('Failed to determine project ID from service account certificate.')
return credentials.Certificate(cert_path), project_id

@pytest.fixture(scope='session')
def project_id(request):
_, project_id = integration_conf(request)
return project_id

@pytest.fixture(autouse=True, scope='session')
def default_app(request):
"""Initializes the default Firebase App instance used for all integration tests.
Expand All @@ -51,7 +56,10 @@ def default_app(request):
test cases having to call it explicitly.
"""
cred, project_id = integration_conf(request)
ops = {'databaseURL' : 'https://{0}.firebaseio.com'.format(project_id)}
ops = {
'databaseURL' : 'https://{0}.firebaseio.com'.format(project_id),
'storageBucket' : '{0}.appspot.com'.format(project_id)
}
return firebase_admin.initialize_app(cred, ops)

@pytest.fixture(scope='session')
Expand Down
44 changes: 44 additions & 0 deletions integration/test_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright 2017 Google Inc.
#
# 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.

"""Integration tests for firebase_admin.storage module."""
import time

from firebase_admin import storage


def test_default_bucket(project_id):
bucket = storage.bucket()
_verify_bucket(bucket, '{0}.appspot.com'.format(project_id))

def test_custom_bucket(project_id):
bucket_name = '{0}.appspot.com'.format(project_id)
bucket = storage.bucket(bucket_name)
_verify_bucket(bucket, bucket_name)

def test_non_existing_bucket():
bucket = storage.bucket('non.existing')
assert bucket.exists() is False

def _verify_bucket(bucket, expected_name):
assert bucket.name == expected_name
file_name = 'data_{0}.txt'.format(int(time.time()))
blob = bucket.blob(file_name)
blob.upload_from_string('Hello World')

blob = bucket.get_blob(file_name)
assert blob.download_as_string() == 'Hello World'

bucket.delete_blob(file_name)
assert not bucket.get_blob(file_name)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ pytest-cov >= 2.4.0
tox >= 2.6.0

google-auth >= 1.0.0
google-cloud-storage >= 1.2.0
requests >= 2.13.0
six >= 1.6.1
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
'to integrate Firebase into their services and applications.')
install_requires = [
'google-auth>=1.0.0',
'google-cloud-storage>=1.2.0',
'requests>=2.13.0',
'six>=1.6.1'
]
Expand Down
15 changes: 15 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import firebase_admin
from firebase_admin import credentials
from firebase_admin import utils
from tests import testutils


Expand Down Expand Up @@ -68,6 +69,11 @@ def get(self):
return None


class AppService(object):
def __init__(self, app):
self._app = app


@pytest.fixture(params=[Cert(), RefreshToken(), ExplicitAppDefault(), ImplicitAppDefault()],
ids=['cert', 'refreshtoken', 'explicit-appdefault', 'implicit-appdefault'])
def app_credential(request):
Expand Down Expand Up @@ -159,3 +165,12 @@ def test_app_delete(self, init_app):
firebase_admin.get_app(init_app.name)
with pytest.raises(ValueError):
firebase_admin.delete_app(init_app)

def test_app_services(self, init_app):
service = utils.get_app_service(init_app, 'test.service', AppService)
assert isinstance(service, AppService)
service2 = utils.get_app_service(init_app, 'test.service', AppService)
assert service is service2
firebase_admin.delete_app(init_app)
with pytest.raises(ValueError):
utils.get_app_service(init_app, 'test.service', AppService)
45 changes: 45 additions & 0 deletions tests/test_storage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright 2017 Google Inc.
#
# 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.

"""Tests for firebase_admin.storage."""

import pytest

import firebase_admin
from firebase_admin import credentials
from firebase_admin import storage
from tests import testutils


def setup_module():
cred = credentials.Certificate(testutils.resource_filename('service_account.json'))
firebase_admin.initialize_app(cred)

def teardown_module():
testutils.cleanup_apps()

def test_invalid_config():
with pytest.raises(ValueError):
storage.bucket()

@pytest.mark.parametrize('name', [None, '', 0, 1, True, False, list(), tuple(), dict()])
def test_invalid_name(name):
with pytest.raises(ValueError):
storage.bucket(name)

def test_valid_name():
# Should not make RPC calls.
bucket = storage.bucket('foo')
assert bucket is not None
assert bucket.name == 'foo'