Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions django_gcp/storage/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ def __init__(
kwargs["default"] = kwargs.pop("default", None)
kwargs["help_text"] = kwargs.pop("help_text", "GCP cloud storage object")

# Note, if you want to define overrides, then use the GCP_STORAGE_EXTRA_STORES
# setting with a different key
# Note: store_key corresponds to a storage alias in the STORAGES setting.
# For example, store_key="versioned" would look up STORAGES["versioned"]["OPTIONS"]
self.storage = GoogleCloudStorage(store_key=store_key)

# We should consider if there's a good use case for customising the storage class:
Expand Down
28 changes: 17 additions & 11 deletions django_gcp/storage/gcloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,24 +335,30 @@ def get_available_name(self, name, max_length=None):


class GoogleCloudMediaStorage(GoogleCloudStorage): # pylint: disable=abstract-method
"""Storage whose bucket name is taken from the GCP_STORAGE_MEDIA_NAME setting
"""Storage for media files, configured via STORAGES['default']

This should be used as the BACKEND for the 'default' storage alias in Django's
STORAGES setting. Configuration options are passed via STORAGES['default']['OPTIONS'].

This actually behaves exactly as a default instantiation of the base
``GoogleCloudStorage`` class, but is there to make configuration more
explicit for first-timers.
``GoogleCloudStorage`` class with store_key='default', but is there to make
configuration more explicit for first-timers.

"""

def __init__(self, **overrides):
if overrides.pop("store_key", "media") != "media":
raise ValueError("You cannot instantiate GoogleCloudMediaStorage with a store_key other than 'media'")
super().__init__(store_key="media", **overrides)
if overrides.pop("store_key", "default") != "default":
raise ValueError("You cannot instantiate GoogleCloudMediaStorage with a store_key other than 'default'")
super().__init__(store_key="default", **overrides)


class GoogleCloudStaticStorage(GoogleCloudStorage): # pylint: disable=abstract-method
"""Storage defined with an appended bucket name (called "<bucket>-static")
"""Storage for static files, configured via STORAGES['staticfiles']

This should be used as the BACKEND for the 'staticfiles' storage alias in Django's
STORAGES setting. Configuration options are passed via STORAGES['staticfiles']['OPTIONS'].

We define that static files are stored in a different bucket than the (private) media files, which:
We recommend storing static files in a different bucket than (private) media files, which:
1. gives us less risk of accidentally setting private files as public
2. allows us easier visual inspection in the console of what's private and what's public static
3. allows us to set blanket public ACLs on the static bucket
Expand All @@ -361,6 +367,6 @@ class GoogleCloudStaticStorage(GoogleCloudStorage): # pylint: disable=abstract-
"""

def __init__(self, **overrides):
if overrides.pop("store_key", "static") != "static":
raise ValueError("You cannot instantiate GoogleCloudStaticStorage with a store_key other than 'static'")
super().__init__(store_key="static", **overrides)
if overrides.pop("store_key", "staticfiles") != "staticfiles":
raise ValueError("You cannot instantiate GoogleCloudStaticStorage with a store_key other than 'staticfiles'")
super().__init__(store_key="staticfiles", **overrides)
30 changes: 22 additions & 8 deletions django_gcp/storage/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,26 @@ def __getattr__(self, setting_key):

@property
def _stores_settings(self):
"""Get a complete dict of all stores defined in settings.py (media + static + extras)"""
all_stores = {
"media": getattr(django_settings, "GCP_STORAGE_MEDIA", None),
"static": getattr(django_settings, "GCP_STORAGE_STATIC", None),
**getattr(django_settings, "GCP_STORAGE_EXTRA_STORES", {}),
}
"""Get a complete dict of all stores defined in settings.py

Reads from Django's STORAGES setting (Django 5.1+). The storage alias (key in STORAGES dict)
is used as the store_key. For example:
- STORAGES["default"] is accessed with store_key="default"
- STORAGES["staticfiles"] is accessed with store_key="staticfiles"
- STORAGES["my-custom"] is accessed with store_key="my-custom"
"""
storages_config = getattr(django_settings, "STORAGES", {})
all_stores = {}

for alias, config in storages_config.items():
backend = config.get("BACKEND", "")

# Only process django_gcp storage backends
if "django_gcp.storage" in backend:
options = config.get("OPTIONS", {})
all_stores[alias] = options

return dict((k, v) for k, v in all_stores.items() if v is not None)
return all_stores

@property
def _store_settings(self):
Expand All @@ -79,7 +91,9 @@ def _store_settings(self):
return self._stores_settings[self._store_key]
except KeyError as e:
raise ImproperlyConfigured(
f"Mismatch: specified store key '{self._store_key}' does not match 'media', 'static', or any store key defined in GCP_STORAGE_EXTRA_STORES"
f"Storage with key '{self._store_key}' not found in STORAGES setting. "
f"Available stores: {list(self._stores_settings.keys())}. "
f"Please ensure STORAGES['{self._store_key}'] or STORAGES['default'/'staticfiles'] is configured with a django_gcp.storage backend."
) from e

def _handle_settings_changed(self, **kwargs):
Expand Down
173 changes: 147 additions & 26 deletions docs/source/storage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,32 +55,122 @@ Setup Media and Static Storage
------------------------------

The most common types of storage are for media and static files, using the storage backend.
We derived a custom storage type for each, making it easier to name them.
We provide custom storage classes for each, making it easier to configure them.

In your ``settings.py`` file, do:
In your ``settings.py`` file, configure the ``STORAGES`` setting (Django 5.1+):

.. code-block:: python

STORAGES = {
"default": {
"BACKEND": "django_gcp.storage.GoogleCloudMediaStorage",
"OPTIONS": {
"bucket_name": "app-assets-environment-media",
"base_url": "https://storage.googleapis.com/app-assets-environment-media/",
},
},
"staticfiles": {
"BACKEND": "django_gcp.storage.GoogleCloudStaticStorage",
"OPTIONS": {
"bucket_name": "app-assets-environment-static",
"base_url": "https://storage.googleapis.com/app-assets-environment-static/",
},
},
}

.. note::
The ``base_url`` option specifies the URL prefix for accessing files. If you omit it,
Django will use the ``MEDIA_URL`` setting for the 'default' storage and ``STATIC_URL``
for the 'staticfiles' storage. Using ``base_url`` in OPTIONS keeps all storage
configuration in one place and prevents URL/bucket_name drift.

You can customise the base URLs to use your own CDN, eg ``https://static.example.com/``


Migrating from Django <5.1
---------------------------

If you're upgrading from an earlier version of django-gcp that used Django <5.1, you'll need to migrate your settings
from the old ``DEFAULT_FILE_STORAGE``, ``STATICFILES_STORAGE``, and ``GCP_STORAGE_*`` format to the new ``STORAGES`` format.

**Before (Django <5.1):**

.. code-block:: python

# Set the default storage (for media files)
DEFAULT_FILE_STORAGE = "django_gcp.storage.GoogleCloudMediaStorage"
GCP_STORAGE_MEDIA = {
"bucket_name": "app-assets-environment-media" # Or whatever name you chose
"bucket_name": "my-media-bucket",
"location": "media/",
}

# Set the static file storage
# This allows `manage.py collectstatic` to automatically upload your static files
STATICFILES_STORAGE = "django_gcp.storage.GoogleCloudStaticStorage"
GCP_STORAGE_STATIC = {
"bucket_name": "app-assets-environment-static" # or whatever name you chose
"bucket_name": "my-static-bucket",
}

GCP_STORAGE_EXTRA_STORES = {
"versioned": {
"bucket_name": "my-versioned-bucket",
}
}

# Point the urls to the store locations
# You could customise the base URLs later with your own cdn, eg https://static.you.com
# But that's only if you feel like being ultra fancy
MEDIA_URL = f"https://storage.googleapis.com/{GCP_STORAGE_MEDIA_NAME}/"
MEDIA_ROOT = "/media/"
STATIC_URL = f"https://storage.googleapis.com/{GCP_STORAGE_STATIC_NAME}/"
STATIC_ROOT = "/static/"
**After (Django 5.1+):**

.. code-block:: python

STORAGES = {
"default": {
"BACKEND": "django_gcp.storage.GoogleCloudMediaStorage",
"OPTIONS": {
"bucket_name": "my-media-bucket",
"base_url": "https://storage.googleapis.com/my-media-bucket/",
"location": "media/",
},
},
"staticfiles": {
"BACKEND": "django_gcp.storage.GoogleCloudStaticStorage",
"OPTIONS": {
"bucket_name": "my-static-bucket",
"base_url": "https://storage.googleapis.com/my-static-bucket/",
},
},
"versioned": {
"BACKEND": "django_gcp.storage.GoogleCloudStorage",
"OPTIONS": {
"bucket_name": "my-versioned-bucket",
"base_url": "https://storage.googleapis.com/my-versioned-bucket/",
},
},
}

Key changes:

- ``DEFAULT_FILE_STORAGE`` → ``STORAGES["default"]["BACKEND"]``
- ``STATICFILES_STORAGE`` → ``STORAGES["staticfiles"]["BACKEND"]``
- ``GCP_STORAGE_MEDIA`` → ``STORAGES["default"]["OPTIONS"]``
- ``GCP_STORAGE_STATIC`` → ``STORAGES["staticfiles"]["OPTIONS"]``
- ``GCP_STORAGE_EXTRA_STORES`` → additional entries in ``STORAGES`` dict
- ``MEDIA_URL``/``STATIC_URL`` → ``base_url`` in ``OPTIONS`` (recommended to keep config in one place)

**BlobField store_key changes:**

If you use ``BlobField`` in your models, you must update the ``store_key`` parameter:

- ``store_key="media"`` → ``store_key="default"``
- ``store_key="static"`` → ``store_key="staticfiles"``
- Extra stores now use the STORAGES alias directly (e.g., ``store_key="versioned"`` matches ``STORAGES["versioned"]``)

Example:

.. code-block:: python

# OLD
blob = BlobField(store_key="media", ...)

# NEW
blob = BlobField(store_key="default", ...)

Note that project-level settings like ``GCP_PROJECT_ID`` and ``GCP_CREDENTIALS`` remain at the root level of your settings file.


Default and Extra stores
Expand All @@ -92,19 +182,45 @@ Default and Extra stores

Any number of extra stores can be added, each corresponding to a different bucket in GCS.

You'll need to give each one a "storage key" to identify it. In your ``settings.py``, include extra stores as:
Simply add additional entries to the ``STORAGES`` dict. Each storage alias can be used to identify
the storage backend you want to use. In your ``settings.py``:

.. code-block:: python

GCP_STORAGE_EXTRA_STORES = {
"my_fun_store_key": {
"bucket_name": "all-the-fun-datafiles"
STORAGES = {
"default": {
"BACKEND": "django_gcp.storage.GoogleCloudMediaStorage",
"OPTIONS": {
"bucket_name": "my-media-bucket",
"base_url": "https://storage.googleapis.com/my-media-bucket/",
},
},
"staticfiles": {
"BACKEND": "django_gcp.storage.GoogleCloudStaticStorage",
"OPTIONS": {
"bucket_name": "my-static-bucket",
"base_url": "https://storage.googleapis.com/my-static-bucket/",
},
},
"my-fun-store": {
"BACKEND": "django_gcp.storage.GoogleCloudStorage",
"OPTIONS": {
"bucket_name": "all-the-fun-datafiles",
"base_url": "https://storage.googleapis.com/all-the-fun-datafiles/",
},
},
"my-sad-store": {
"BACKEND": "django_gcp.storage.GoogleCloudStorage",
"OPTIONS": {
"bucket_name": "all-the-sad-datafiles",
"base_url": "https://storage.googleapis.com/all-the-sad-datafiles/",
},
},
"my_sad_store_key": {
"bucket_name": "all-the-sad-datafiles"
}
}

For extra stores, you can access them using ``BlobField(store_key="my-fun-store")`` or by
using ``storages["my-fun-store"]`` in your code.


.. group-tab:: Default Storage

Expand Down Expand Up @@ -254,16 +370,21 @@ Works as a standard drop-in storage backend.
Storage Settings Options
------------------------

Each store can be set up with different options, passed within the dict given to ``GCP_STORAGE_MEDIA``, ``GCP_STORAGE_STATIC`` or within the dicts given to ``GCP_STORAGE_EXTRA_STORES``.
Each store can be set up with different options, passed within the ``OPTIONS`` dict for each storage backend in the ``STORAGES`` setting.

For example, to set the media storage up so that files go to a different location than the root of the bucket, you'd use:

.. code-block:: python

GCP_STORAGE_MEDIA = {
"bucket_name": "app-assets-environment-media"
"location": "not/the/bucket/root/",
# ... and whatever other options you want
STORAGES = {
"default": {
"BACKEND": "django_gcp.storage.GoogleCloudMediaStorage",
"OPTIONS": {
"bucket_name": "app-assets-environment-media",
"location": "not/the/bucket/root/",
# ... and whatever other options you want
},
},
}

The full range of options (and their defaults, which apply to all stores) is as follows:
Expand Down
52 changes: 52 additions & 0 deletions docs/source/version_history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,55 @@ our `conventional commits tools <https://github/octue/conventional-commits>`_
for completely automating code versions, release numbering and release history.

So for a full version history, check our `releases page <https://github/octue/django-gcp/releases>`_.

Breaking Changes in Next Release
---------------------------------

**Django 5.1+ STORAGES Setting Migration**

This release drops support for Django <5.1 and migrates to Django's new ``STORAGES`` setting format.

**What changed:**

- **Minimum Django version**: Now requires Django >=5.1,<6.0 (was >=4.0,<5.1)
- **Settings format**: The old ``DEFAULT_FILE_STORAGE``, ``STATICFILES_STORAGE``, ``GCP_STORAGE_MEDIA``, ``GCP_STORAGE_STATIC``, and ``GCP_STORAGE_EXTRA_STORES`` settings are no longer supported
- **New format**: Use Django's ``STORAGES`` dict setting (introduced in Django 4.2, required in Django 5.1+)
- **BlobField store_key**: Must be updated to use Django's storage aliases (``"default"``, ``"staticfiles"``, etc.)

**Migration guide:**

See the :ref:`storage` documentation for complete migration instructions. In summary:

Old settings::

DEFAULT_FILE_STORAGE = "django_gcp.storage.GoogleCloudMediaStorage"
GCP_STORAGE_MEDIA = {"bucket_name": "my-media"}
STATICFILES_STORAGE = "django_gcp.storage.GoogleCloudStaticStorage"
GCP_STORAGE_STATIC = {"bucket_name": "my-static"}

New settings::

STORAGES = {
"default": {
"BACKEND": "django_gcp.storage.GoogleCloudMediaStorage",
"OPTIONS": {
"bucket_name": "my-media",
"base_url": "https://storage.googleapis.com/my-media/",
},
},
"staticfiles": {
"BACKEND": "django_gcp.storage.GoogleCloudStaticStorage",
"OPTIONS": {
"bucket_name": "my-static",
"base_url": "https://storage.googleapis.com/my-static/",
},
},
}

BlobField changes::

# OLD
blob = BlobField(store_key="media", ...)

# NEW
blob = BlobField(store_key="default", ...)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ packages = [{ include = "django_gcp" },]


[tool.poetry.dependencies]
Django = ">=4.0,<5.1"
Django = ">=5.1,<6.0"
python = ">=3.9,<4"
django-app-settings = "^0.7.1"
python-dateutil = "^2.8.2"
Expand Down
Loading
Loading