Configure Django and Google Cloud Storage?

May, 2022 Update:

With this instruction, you can connect your Django app to your bucket on GCS(Google Cloud Storage) and you can serve your static files and serve, upload and delete your media files.

For example, you have the bucket "my-django-bucket" on GCS:

enter image description here

And you have the service account "my-django-bucket-sa" then you need to copy(Ctrl+C) the email "[email protected]":

enter image description here

Next, in Bucket details of the bucket "my-django-bucket", click on "PERMISSIONS" then "ADD":

enter image description here

Then, to enable the service account "my-django-bucket-sa" to have the full control of GCS resources, paste(Ctrl+V) the email "[email protected]" to "New principals" then choose the role "Storage Admin" then click on "SAVE". *Choose other role by checking IAM roles for Cloud Storage if you don't want the role "Storage Admin" which can have the full control of GCS resources:

enter image description here

Next, to enable all users to view(read) files, type "allUsers" to “New principals” then choose the role "Storage Legacy Object Reader" then click on "SAVE":

enter image description here

Then, you should be asked as shown below so click on "ALLOW PUBLIC ACCESS":

enter image description here

Finally, you could add the role "Storage Admin" to the service account "my-django-bucket-sa" and the role "Storage Legacy Object Reader" to "allUsers":

enter image description here

Next, you need to download the private key of the service account "my-django-bucket-sa" in JSON so click on "Manage keys" from the 3 dots "⋮":

enter image description here

Then, click on "Create new key" from "ADD KEY":

enter image description here

Then, choose "JSON" then click on "CREATE"

enter image description here

Finally, you could download the private key of the service account "my-django-bucket-sa" in JSON "myproject-347313-020754294843.json":

enter image description here

Now, you have a django project and there are one settings folder "core" which has "static/core/core.js" and "settings.py" and one application folder "myapp" which has "static/myapp/myapp.css" as shown below:

enter image description here

Then next, you need to put "myproject-347313-020754294843.json" to the root django project directory where "db.sqlite3" and "manage.py" are:

enter image description here

Then, you better rename "myproject-347313-020754294843.json" to a shorter and reasonable name such as "gcpCredentials.json":

enter image description here

Next, you need to install "django-storages[google]" to connect to and communicate with "my-django-bucket" on GCS(Google Cloud Storage):

pip install django-storages[google]

By installing "django-storages[google]", you can get "django-storages" and other necessary packages as shown below:

"requirements.txt"

django-storages==1.12.3
cachetools==4.2.4
google-api-core==2.7.2
google-auth==2.6.5
google-cloud-core==2.3.0
google-cloud-storage==2.0.0
google-crc32c==1.3.0
google-resumable-media==2.3.2
googleapis-common-protos==1.56.0
protobuf==3.19.4
pyasn1==0.4.8
pyasn1-modules==0.2.8

Be careful, if you install "django-storages" without "[google]" as shown below:

pip install django-storages

You can only get "django-storages" as shown below.

"requirements.txt"

django-storages==1.12.3

Next, create "gcsUtils.py" in "core" folder where "settings.py" is:

enter image description here

Then, put this code below to "gcsUtils.py" to define "Static" and "Media" variables which each have a "GoogleCloudStorage" class object:

# "core/gcsUtils.py"

from storages.backends.gcloud import GoogleCloudStorage

Static = lambda: GoogleCloudStorage(location='static')
Media = lambda: GoogleCloudStorage(location='media')

Next, add this code below to "settings.py". *"STATICFILES_STORAGE" is like the conbination of "STATIC_ROOT" and "STATIC_URL" and "DEFAULT_FILE_STORAGE" is like the conbination of "MEDIA_ROOT" and "MEDIA_URL":

# "core/settings.py"

from google.oauth2 import service_account

# Set "static" folder
STATICFILES_STORAGE = 'core.gcsUtils.Static'

# Set "media" folder
DEFAULT_FILE_STORAGE = 'core.gcsUtils.Media'

GS_BUCKET_NAME = 'my-django-bucket'

# Add an unique ID to a file name if same file name exists
GS_FILE_OVERWRITE = False

GS_CREDENTIALS = service_account.Credentials.from_service_account_file(
    os.path.join(BASE_DIR, 'gcpCredentials.json'),
)

Then, run this command below:

python manage.py collectstatic

Now, "static" folder is created in "my-django-bucket":

enter image description here

And static files are collected from "admin" and "application" folders to "static" folder in "my-django-bucket":

enter image description here

And this is "myapp.css" in "myapp" folder:

enter image description here

But as you can see, static files are not collected from the settings folder "core" to "static" folder in "my-django-bucket":

enter image description here

Because "STATICFILES_STORAGE" can only collect static files from "admin" and "application" folders but not from other folders like the settings folder "core":

# "core/settings.py"

STATICFILES_STORAGE = 'core.gcsUtils.Static'

So, to collect static files from the settings folder "core" to "static" folder in "my-django-bucket":

enter image description here

You need to add "STATICFILES_DIRS" to "settings.py" as shown below:

# "core/settings.py"

# Collect static files from the settings folder
# "core" which is not "admin" and "application" folder
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'core/static'),
]

Then, this is the full code of "settings.py":

# "core/settings.py"

from google.oauth2 import service_account

# Collect static files from the settings folder
# "core" which is not "admin" and "application" folder
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'core/static'),
]

# Set "static" folder
STATICFILES_STORAGE = 'core.gcsUtils.Static'

# Set "media" folder
DEFAULT_FILE_STORAGE = 'core.gcsUtils.Media'

GS_BUCKET_NAME = 'my-django-bucket'

# Add an unique ID to a file name if same file name exists
GS_FILE_OVERWRITE = False

GS_CREDENTIALS = service_account.Credentials.from_service_account_file(
    os.path.join(BASE_DIR, 'gcpCredentials.json'),
)

Then again, run this command below:

python manage.py collectstatic

Then, static files are collected from the settings folder "core" to "static" folder in "my-django-bucket":

enter image description here

And this is "core.js" in "core" folder:

enter image description here

Next, this is the code for "myapp/models.py":

# "myapp/models.py"

from django.db import models

class Image(models.Model):
    image = models.ImageField(upload_to='images/fruits')
    
    def  __str__(self):
        return  str(self.image)

And this is the code for "myapp/admin.py":

# "myapp/admin.py"

from django.contrib import admin
from .models import Image

admin.site.register(Image)

Then, upload "orange.jpg":

enter image description here

Now, "media" folder is created in "my-django-bucket":

enter image description here

And "orange.jpg" is uploaded in "media/images/fruits":

enter image description here

And because "GS_FILE_OVERWRITE = False" is set in "settings.py":

# "core/settings.py"

# Add an unique ID to a file name if same file name exists
GS_FILE_OVERWRITE = False

If uploading the same name file "orange.jpg" again:

enter image description here

Then, the unique ID "_VPJxGBW" is added to "orange.jpg" to prevent file overwrite then, "orange_VPJxGBW.jpg" is uploaded as shown below:

enter image description here

Next, if there is "orange.jpg" in "media/images/fruits":

enter image description here

Then, update(change) "orange.jpg" to "apple.jpg" being uploaded:

enter image description here

Then, "apple.jpg" is uploaded in "media/images/fruits" but "orange.jpg" is still in "media/images/fruits" without deleted:

enter image description here

And if there is "orange.jpg" in "media/images/fruits":

enter image description here

Then, delete "orange.jpg":

enter image description here

But "orange.jpg" is still in "media/images/fruits" without deleted:

enter image description here

So, to delete uploaded files when they are updated(changed) and deleted, you need to install "django-cleanup":

pip install django-cleanup

Then, add it to the bottom of "INSTALLED_APPS" in "settings.py":

# "core/settings.py"

INSTALLED_APPS = (
    ...,
    'django_cleanup.apps.CleanupConfig', # Here
)

Then, if there is "orange.jpg" in "media/images/fruits":

enter image description here

Then, update(change) "orange.jpg" to "apple.jpg" being uploaded:

enter image description here

Then, "apple.jpg" is uploaded in "media/images/fruits" and "orange.jpg" is deleted from "media/images/fruits":

enter image description here

And if there is "orange.jpg" in "media/images/fruits":

enter image description here

Then, delete "orange.jpg":

enter image description here

Then, "orange.jpg" is deleted from "media/images/fruits":

enter image description here

Lastly, the GCS Bucket Settings which you have just set in "settings.py" as shown below work in both "DEBUG = True" and "DEBUG = False":

# "core/settings.py"

from google.oauth2 import service_account

# Collect static files from the settings folder
# "core" which is not "admin" and "application" folder
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'core/static'),
]

# Set "static" folder
STATICFILES_STORAGE = 'core.gcsUtils.Static'

# Set "media" folder
DEFAULT_FILE_STORAGE = 'core.gcsUtils.Media'

GS_BUCKET_NAME = 'my-django-bucket'

# Add an unique ID to a file name if same file name exists
GS_FILE_OVERWRITE = False

GS_CREDENTIALS = service_account.Credentials.from_service_account_file(
    os.path.join(BASE_DIR, 'gcpCredentials.json'),
)

And, there will be "STATIC_ROOT", "STATIC_URL", "MEDIA_ROOT", "MEDIA_URL" with the GCS Bucket Settings in "settings.py" as shown below. So, in this case, "STATIC_ROOT", "STATIC_URL", "MEDIA_ROOT" and "MEDIA_URL" don't work while the GCS Bucket Settings work communicating with "my-django-bucket" on GCS:

# "core/settings.py"

from google.oauth2 import service_account

STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATIC_URL = '/static/'
  
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

'''GCS Bucket Settings Start'''
# Collect static files from the settings folder
# "core" which is not "admin" and "application" folder
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'core/static'),
]

# Set "static" folder
STATICFILES_STORAGE = 'core.gcsUtils.Static'

# Set "media" folder
DEFAULT_FILE_STORAGE = 'core.gcsUtils.Media'

GS_BUCKET_NAME = 'my-django-bucket'

# Add an unique ID to a file name if same file name exists
GS_FILE_OVERWRITE = False

GS_CREDENTIALS = service_account.Credentials.from_service_account_file(
    os.path.join(BASE_DIR, 'gcpCredentials.json'),
)
'''GCS Bucket Settings End'''

So, if you want "STATIC_ROOT", "STATIC_URL", "MEDIA_ROOT" and "MEDIA_URL" to work, just comment the GCS Bucket Settings then set "STATICFILES_DIRS" as shown below:

# "core/settings.py"

from google.oauth2 import service_account

# Collect static files from the settings folder
# "core" which is not "admin" and "application" folder
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'core/static'),
]

STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATIC_URL = '/static/'
  
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

"""
'''GCS Bucket Settings Start'''
# Collect static files from the settings folder
# "core" which is not "admin" and "application" folder
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'core/static'),
]

# Set "static" folder
STATICFILES_STORAGE = 'core.gcsUtils.Static'

# Set "media" folder
DEFAULT_FILE_STORAGE = 'core.gcsUtils.Media'

GS_BUCKET_NAME = 'my-django-bucket'

# Add an unique ID to a file name if same file name exists
GS_FILE_OVERWRITE = False

GS_CREDENTIALS = service_account.Credentials.from_service_account_file(
    os.path.join(BASE_DIR, 'gcpCredentials.json'),
)
'''GCS Bucket Settings End'''
"""

Django-storages has a backend for Google Cloud Storage, but it is not documented, I realised looking in the repo. Got it working with this setup:

DEFAULT_FILE_STORAGE = 'storages.backends.gs.GSBotoStorage'
GS_ACCESS_KEY_ID = 'YourID'
GS_SECRET_ACCESS_KEY = 'YourKEY'
GS_BUCKET_NAME = 'YourBucket'
STATICFILES_STORAGE = 'storages.backends.gs.GSBotoStorage'

To get YourKEY and YourID you should create Interoperability keys, in the settings tab.

Hope it helps and you don't have to learn it the hard way :)

Ah in case you haven't yet, the dependencies are:

pip install django-storages
pip install boto

Django-storages is, in fact, a viable alternative. You must be careful with it's Google Cloud backend though as the url() method it provides causes unnecessary HTTP calls to Google. (Django calls .url() when rendering static files, for example).

https://github.com/jschneier/django-storages/issues/491

settings.py

    DEFAULT_FILE_STORAGE = 'config.storage_backends.GoogleCloudMediaStorage'
    STATICFILES_STORAGE = 'config.storage_backends.GoogleCloudStaticStorage'
    GS_PROJECT_ID = '<google-cloud-project-id>'
    GS_MEDIA_BUCKET_NAME = '<name-of-static-bucket>'
    GS_STATIC_BUCKET_NAME = '<name-of-static-bucket>'
    STATIC_URL = 'https://storage.googleapis.com/{}/'.format(GS_STATIC_BUCKET_NAME)
    MEDIA_URL = 'https://storage.googleapis.com/{}/'.format(GS_MEDIA_BUCKET_NAME)

storage_backends.py

    """
    GoogleCloudStorage extensions suitable for handing Django's
    Static and Media files.

    Requires following settings:
    MEDIA_URL, GS_MEDIA_BUCKET_NAME
    STATIC_URL, GS_STATIC_BUCKET_NAME

    In addition to
    https://django-storages.readthedocs.io/en/latest/backends/gcloud.html
    """
    from django.conf import settings
    from storages.backends.gcloud import GoogleCloudStorage
    from storages.utils import setting
    from urllib.parse import urljoin


    class GoogleCloudMediaStorage(GoogleCloudStorage):
        """GoogleCloudStorage suitable for Django's Media files."""

        def __init__(self, *args, **kwargs):
            if not settings.MEDIA_URL:
                raise Exception('MEDIA_URL has not been configured')
            kwargs['bucket_name'] = setting('GS_MEDIA_BUCKET_NAME', strict=True)
            super(GoogleCloudMediaStorage, self).__init__(*args, **kwargs)

        def url(self, name):
            """.url that doesn't call Google."""
            return urljoin(settings.MEDIA_URL, name)


    class GoogleCloudStaticStorage(GoogleCloudStorage):
        """GoogleCloudStorage suitable for Django's Static files"""

        def __init__(self, *args, **kwargs):
            if not settings.STATIC_URL:
                raise Exception('STATIC_URL has not been configured')
            kwargs['bucket_name'] = setting('GS_STATIC_BUCKET_NAME', strict=True)
            super(GoogleCloudStaticStorage, self).__init__(*args, **kwargs)

        def url(self, name):
            """.url that doesn't call Google."""
            return urljoin(settings.STATIC_URL, name)

Note: authentication is handled by default via the GOOGLE_APPLICATION_CREDENTIALS environment variable.

https://cloud.google.com/docs/authentication/production#setting_the_environment_variable


So, this basically will work. (With this library and settings).

The trick to making it work, is knowing where to get the 'user' and 'key' parameters for libcloud.

On Google Cloud Console > Storage, click Settings. Then click on the right-hand tab called Interoperability. On that panel, is a lone button, which says something like Enable Interoperability. Click it.

Voila! You now have a username and key.


Note: Do not use django-storages from pypi. It has not been updated, and doesn't work with recent releases of Django.

Use this version:

pip install -e 'git+https://github.com/jschneier/django-storages.git#egg=django-storages'


Edit: If you want to use a reverse proxy, then you may consider my slightly modified version. https://github.com/jschneier/django-storages/compare/master...halfnibble:master

Description: Under certain circumstances, it may be necessary to load files using a reverse proxy. This could be used to alleviate cross-origin request errors.

This small PR allows the developer to set an optional LIBCLOUD_PROXY_URL in settings.py.

Example Usage

# Apache VirtualHost conf
ProxyPass /foo http://storage.googleapis.com
ProxyPassReverse /foo http://storage.googleapis.com


# settings.py
LIBCLOUD_PROXY_URL = '/foo/'