From a2f889800dcbf1a6342610757b45129acc77cbd7 Mon Sep 17 00:00:00 2001 From: Martey Dodoo Date: Fri, 11 Jan 2019 10:23:30 -0500 Subject: [PATCH] Allow custom endpoints with Google Cloud Storage. Add new `GS_CUSTOM_ENDPOINT` setting to allow usage of custom domains/URLs with Google Cloud Storage, similar to `AWS_S3_CUSTOM_DOMAIN`. This upgrades the minimum version of google-cloud-storage to 1.15.0. --- AUTHORS | 1 + docs/backends/gcloud.rst | 5 +++++ setup.py | 2 +- storages/backends/gcloud.py | 20 ++++++++++++++++++-- tests/test_gcloud.py | 12 ++++++++++++ 5 files changed, 37 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index 5f09414b4..fd4ab68ff 100644 --- a/AUTHORS +++ b/AUTHORS @@ -37,6 +37,7 @@ By order of apparition, thanks: * Jumpei Yoshimura (S3 docs) * Jon Dufresne * Rodrigo Gadea (Dropbox fixes) + * Martey Dodoo diff --git a/docs/backends/gcloud.rst b/docs/backends/gcloud.rst index 38be7f71f..9849dafb8 100644 --- a/docs/backends/gcloud.rst +++ b/docs/backends/gcloud.rst @@ -149,6 +149,11 @@ must fit in memory. Recommended if you are going to be uploading large files. Sets Cache-Control HTTP header for the file, more about HTTP caching can be found `here `_ +``GS_CUSTOM_ENDPOINT`` (optional: default is ``None``) + +Sets a `custom endpoint `_, +that will be used instead of ``https://storage.googleapis.com`` when generating URLs for files. + ``GS_LOCATION`` (optional: default is ``''``) Subdirectory in which the files will be stored. diff --git a/setup.py b/setup.py index 9b0b405e0..df1d1944b 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ def read(filename): 'boto': ['boto>=2.32.0'], 'boto3': ['boto3>=1.4.4'], 'dropbox': ['dropbox>=7.2.1'], - 'google': ['google-cloud-storage>=0.22.0'], + 'google': ['google-cloud-storage>=1.15.0'], 'libcloud': ['apache-libcloud'], 'sftp': ['paramiko'], }, diff --git a/storages/backends/gcloud.py b/storages/backends/gcloud.py index 5b2af3e84..7bce639f2 100644 --- a/storages/backends/gcloud.py +++ b/storages/backends/gcloud.py @@ -8,6 +8,7 @@ from django.utils import timezone from django.utils.deconstruct import deconstructible from django.utils.encoding import force_bytes, smart_str +from django.utils.six.moves.urllib.parse import quote from storages.utils import ( check_location, clean_name, get_available_overwrite_name, safe_join, @@ -15,6 +16,7 @@ ) try: + from google.cloud.storage._signing import generate_signed_url_v2 from google.cloud.storage import Blob, Client from google.cloud.exceptions import Conflict, NotFound except ImportError: @@ -88,6 +90,7 @@ class GoogleCloudStorage(Storage): project_id = setting('GS_PROJECT_ID') credentials = setting('GS_CREDENTIALS') bucket_name = setting('GS_BUCKET_NAME') + custom_endpoint = setting('GS_CUSTOM_ENDPOINT', None) location = setting('GS_LOCATION', '') auto_create_bucket = setting('GS_AUTO_CREATE_BUCKET', False) auto_create_acl = setting('GS_AUTO_CREATE_ACL', 'projectPrivate') @@ -263,9 +266,22 @@ def url(self, name): name = self._normalize_name(clean_name(name)) blob = self.bucket.blob(self._encode_name(name)) - if self.default_acl == 'publicRead': + if not self.custom_endpoint and self.default_acl == 'publicRead': return blob.public_url - return blob.generate_signed_url(self.expiration) + elif self.default_acl == 'publicRead': + return '{storage_base_url}/{quoted_name}'.format( + storage_base_url=self.custom_endpoint, + quoted_name=quote(name.encode('utf-8')), + ) + elif not self.custom_endpoint: + return blob.generate_signed_url(self.expiration) + else: + return generate_signed_url_v2( + self.credentials, + resource=quote(name.encode('utf-8')), + api_access_endpoint=self.custom_endpoint, + expiration=self.expiration, + ) def get_available_name(self, name, max_length=None): name = clean_name(name) diff --git a/tests/test_gcloud.py b/tests/test_gcloud.py index 60a895108..79b426814 100644 --- a/tests/test_gcloud.py +++ b/tests/test_gcloud.py @@ -372,6 +372,18 @@ def test_url_not_public_file_with_custom_expires(self): self.assertEqual(url, 'http://signed_url') blob.generate_signed_url.assert_called_with(timedelta(seconds=3600)) + def test_custom_endpoint(self): + self.storage.custom_endpoint = "https://example.com" + + self.storage.default_acl = 'publicRead' + url = "{}/{}".format(self.storage.custom_endpoint, self.filename) + self.assertEqual(self.storage.url(self.filename), url) + + signed_url = 'https://signed_url' + self.storage.default_acl = 'projectPrivate' + gcloud.generate_signed_url_v2 = mock.MagicMock(return_value=signed_url) + self.assertEqual(self.storage.url(self.filename), signed_url) + def test_get_available_name(self): self.storage.file_overwrite = True self.assertEqual(self.storage.get_available_name(