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:
And you have the service account "my-django-bucket-sa" then you need to copy(Ctrl+C) the email "[email protected]":
Next, in Bucket details of the bucket "my-django-bucket", click on "PERMISSIONS" then "ADD":
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:
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":
Then, you should be asked as shown below so click on "ALLOW PUBLIC ACCESS":
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":
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 "⋮":
Then, click on "Create new key" from "ADD KEY":
Then, choose "JSON" then click on "CREATE"
Finally, you could download the private key of the service account "my-django-bucket-sa" in JSON "myproject-347313-020754294843.json":
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:
Then next, you need to put "myproject-347313-020754294843.json" to the root django project directory where "db.sqlite3" and "manage.py" are:
Then, you better rename "myproject-347313-020754294843.json" to a shorter and reasonable name such as "gcpCredentials.json":
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:
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":
And static files are collected from "admin" and "application" folders to "static" folder in "my-django-bucket":
And this is "myapp.css" in "myapp" folder:
But as you can see, static files are not collected from the settings folder "core" to "static" folder in "my-django-bucket":
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":
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":
And this is "core.js" in "core" folder:
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":
Now, "media" folder is created in "my-django-bucket":
And "orange.jpg" is uploaded in "media/images/fruits":
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:
Then, the unique ID "_VPJxGBW" is added to "orange.jpg" to prevent file overwrite then, "orange_VPJxGBW.jpg" is uploaded as shown below:
Next, if there is "orange.jpg" in "media/images/fruits":
Then, update(change) "orange.jpg" to "apple.jpg" being uploaded:
Then, "apple.jpg" is uploaded in "media/images/fruits" but "orange.jpg" is still in "media/images/fruits" without deleted:
And if there is "orange.jpg" in "media/images/fruits":
Then, delete "orange.jpg":
But "orange.jpg" is still in "media/images/fruits" without deleted:
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":
Then, update(change) "orange.jpg" to "apple.jpg" being uploaded:
Then, "apple.jpg" is uploaded in "media/images/fruits" and "orange.jpg" is deleted from "media/images/fruits":
And if there is "orange.jpg" in "media/images/fruits":
Then, delete "orange.jpg":
Then, "orange.jpg" is deleted from "media/images/fruits":
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/'