mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
18896 Replace STORAGE_BACKEND with STORAGES and support Script running from S3 (#18680)
This commit is contained in:
parent
ffe035567a
commit
1b4e00aeda
@ -42,6 +42,10 @@ django-rich
|
|||||||
# https://github.com/rq/django-rq/blob/master/CHANGELOG.md
|
# https://github.com/rq/django-rq/blob/master/CHANGELOG.md
|
||||||
django-rq
|
django-rq
|
||||||
|
|
||||||
|
# Provides a variety of storage backends
|
||||||
|
# https://github.com/jschneier/django-storages/blob/master/CHANGELOG.rst
|
||||||
|
django-storages
|
||||||
|
|
||||||
# Abstraction models for rendering and paginating HTML tables
|
# Abstraction models for rendering and paginating HTML tables
|
||||||
# https://github.com/jieter/django-tables2/blob/master/CHANGELOG.md
|
# https://github.com/jieter/django-tables2/blob/master/CHANGELOG.md
|
||||||
django-tables2
|
django-tables2
|
||||||
|
@ -54,7 +54,7 @@ pg_dump --username netbox --password --host localhost -s netbox > netbox_schema.
|
|||||||
By default, NetBox stores uploaded files (such as image attachments) in its media directory. To fully replicate an instance of NetBox, you'll need to copy both the database and the media files.
|
By default, NetBox stores uploaded files (such as image attachments) in its media directory. To fully replicate an instance of NetBox, you'll need to copy both the database and the media files.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
These operations are not necessary if your installation is utilizing a [remote storage backend](../configuration/system.md#storage_backend).
|
These operations are not necessary if your installation is utilizing a [remote storage backend](../configuration/system.md#storages).
|
||||||
|
|
||||||
### Archive the Media Directory
|
### Archive the Media Directory
|
||||||
|
|
||||||
|
@ -196,23 +196,46 @@ The dotted path to the desired search backend class. `CachedValueSearchBackend`
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## STORAGE_BACKEND
|
## STORAGES
|
||||||
|
|
||||||
Default: None (local storage)
|
The backend storage engine for handling uploaded files such as [image attachments](../models/extras/imageattachment.md) and [custom scripts](../customization/custom-scripts.md). NetBox integrates with the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) and [`django-storage-swift`](https://github.com/dennisv/django-storage-swift) libraries, which provide backends for several popular file storage services. If not configured, local filesystem storage will be used.
|
||||||
|
|
||||||
The backend storage engine for handling uploaded files (e.g. image attachments). NetBox supports integration with the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) and [`django-storage-swift`](https://github.com/dennisv/django-storage-swift) packages, which provide backends for several popular file storage services. If not configured, local filesystem storage will be used.
|
By default, the following configuration is used:
|
||||||
|
|
||||||
The configuration parameters for the specified storage backend are defined under the `STORAGE_CONFIG` setting.
|
```python
|
||||||
|
STORAGES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||||
|
},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"BACKEND": "extras.storage.ScriptFileSystemStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
Within the `STORAGES` dictionary, `"default"` is used for image uploads, "staticfiles" is for static files and `"scripts"` is used for custom scripts.
|
||||||
|
|
||||||
## STORAGE_CONFIG
|
If using a remote storage like S3, define the config as `STORAGES[key]["OPTIONS"]` for each storage item as needed. For example:
|
||||||
|
|
||||||
Default: Empty
|
```python
|
||||||
|
STORAGES = {
|
||||||
|
"scripts": {
|
||||||
|
"BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
|
||||||
|
"OPTIONS": {
|
||||||
|
'access_key': 'access key',
|
||||||
|
'secret_key': 'secret key',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
A dictionary of configuration parameters for the storage backend configured as `STORAGE_BACKEND`. The specific parameters to be used here are specific to each backend; see the documentation for your selected backend ([`django-storages`](https://django-storages.readthedocs.io/en/stable/) or [`django-storage-swift`](https://github.com/dennisv/django-storage-swift)) for more detail.
|
The specific configuration settings for each storage backend can be found in the [django-storages documentation](https://django-storages.readthedocs.io/en/latest/index.html).
|
||||||
|
|
||||||
If `STORAGE_BACKEND` is not defined, this setting will be ignored.
|
!!! note
|
||||||
|
Any keys defined in the `STORAGES` configuration parameter replace those in the default configuration. It is only necessary to define keys within the `STORAGES` for the specific backend(s) you wish to configure.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -140,6 +140,8 @@ The Script class provides two convenience methods for reading data from files:
|
|||||||
|
|
||||||
These two methods will load data in YAML or JSON format, respectively, from files within the local path (i.e. `SCRIPTS_ROOT`).
|
These two methods will load data in YAML or JSON format, respectively, from files within the local path (i.e. `SCRIPTS_ROOT`).
|
||||||
|
|
||||||
|
**Note:** These convenience methods are deprecated and will be removed in NetBox v4.4. These only work if running scripts within the local path, they will not work if using a storage other than ScriptFileSystemStorage.
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
The Script object provides a set of convenient functions for recording messages at different severity levels:
|
The Script object provides a set of convenient functions for recording messages at different severity levels:
|
||||||
|
@ -207,7 +207,7 @@ All Python packages required by NetBox are listed in `requirements.txt` and will
|
|||||||
|
|
||||||
### Remote File Storage
|
### Remote File Storage
|
||||||
|
|
||||||
By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/system.md#storage_backend) in `configuration.py`.
|
By default, NetBox will use the local filesystem to store uploaded files. To use a remote filesystem, install the [`django-storages`](https://django-storages.readthedocs.io/en/stable/) library and configure your [desired storage backend](../configuration/system.md#storages) in `configuration.py`.
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
sudo sh -c "echo 'django-storages' >> /opt/netbox/local_requirements.txt"
|
sudo sh -c "echo 'django-storages' >> /opt/netbox/local_requirements.txt"
|
||||||
|
@ -357,17 +357,6 @@ class DataFile(models.Model):
|
|||||||
|
|
||||||
return is_modified
|
return is_modified
|
||||||
|
|
||||||
def write_to_disk(self, path, overwrite=False):
|
|
||||||
"""
|
|
||||||
Write the object's data to disk at the specified path
|
|
||||||
"""
|
|
||||||
# Check whether file already exists
|
|
||||||
if os.path.isfile(path) and not overwrite:
|
|
||||||
raise FileExistsError()
|
|
||||||
|
|
||||||
with open(path, 'wb+') as new_file:
|
|
||||||
new_file.write(self.data)
|
|
||||||
|
|
||||||
|
|
||||||
class AutoSyncRecord(models.Model):
|
class AutoSyncRecord(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from functools import cached_property
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.core.files.storage import storages
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from ..choices import ManagedFileRootPathChoices
|
from ..choices import ManagedFileRootPathChoices
|
||||||
|
from extras.storage import ScriptFileSystemStorage
|
||||||
from netbox.models.features import SyncedDataMixin
|
from netbox.models.features import SyncedDataMixin
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
|
|
||||||
@ -76,15 +79,35 @@ class ManagedFile(SyncedDataMixin, models.Model):
|
|||||||
return os.path.join(self._resolve_root_path(), self.file_path)
|
return os.path.join(self._resolve_root_path(), self.file_path)
|
||||||
|
|
||||||
def _resolve_root_path(self):
|
def _resolve_root_path(self):
|
||||||
|
storage = self.storage
|
||||||
|
if isinstance(storage, ScriptFileSystemStorage):
|
||||||
return {
|
return {
|
||||||
'scripts': settings.SCRIPTS_ROOT,
|
'scripts': settings.SCRIPTS_ROOT,
|
||||||
'reports': settings.REPORTS_ROOT,
|
'reports': settings.REPORTS_ROOT,
|
||||||
}[self.file_root]
|
}[self.file_root]
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
def sync_data(self):
|
def sync_data(self):
|
||||||
if self.data_file:
|
if self.data_file:
|
||||||
self.file_path = os.path.basename(self.data_path)
|
self.file_path = os.path.basename(self.data_path)
|
||||||
self.data_file.write_to_disk(self.full_path, overwrite=True)
|
self._write_to_disk(self.full_path, overwrite=True)
|
||||||
|
|
||||||
|
def _write_to_disk(self, path, overwrite=False):
|
||||||
|
"""
|
||||||
|
Write the object's data to disk at the specified path
|
||||||
|
"""
|
||||||
|
# Check whether file already exists
|
||||||
|
storage = self.storage
|
||||||
|
if storage.exists(path) and not overwrite:
|
||||||
|
raise FileExistsError()
|
||||||
|
|
||||||
|
with storage.open(path, 'wb+') as new_file:
|
||||||
|
new_file.write(self.data)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def storage(self):
|
||||||
|
return storages.create_storage(storages.backends["scripts"])
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
@ -104,8 +127,9 @@ class ManagedFile(SyncedDataMixin, models.Model):
|
|||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
# Delete file from disk
|
# Delete file from disk
|
||||||
|
storage = self.storage
|
||||||
try:
|
try:
|
||||||
os.remove(self.full_path)
|
storage.delete(self.full_path)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.storage import storages
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from core.forms import ManagedFileForm
|
||||||
from extras.choices import DurationChoices
|
from extras.choices import DurationChoices
|
||||||
|
from extras.storage import ScriptFileSystemStorage
|
||||||
from utilities.forms.widgets import DateTimePicker, NumberWithOptions
|
from utilities.forms.widgets import DateTimePicker, NumberWithOptions
|
||||||
from utilities.datetime import local_now
|
from utilities.datetime import local_now
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'ScriptFileForm',
|
||||||
'ScriptForm',
|
'ScriptForm',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -55,3 +62,26 @@ class ScriptForm(forms.Form):
|
|||||||
self.cleaned_data['_schedule_at'] = local_now()
|
self.cleaned_data['_schedule_at'] = local_now()
|
||||||
|
|
||||||
return self.cleaned_data
|
return self.cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptFileForm(ManagedFileForm):
|
||||||
|
"""
|
||||||
|
ManagedFileForm with a custom save method to use django-storages.
|
||||||
|
"""
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
# If a file was uploaded, save it to disk
|
||||||
|
if self.cleaned_data['upload_file']:
|
||||||
|
storage = storages.create_storage(storages.backends["scripts"])
|
||||||
|
|
||||||
|
filename = self.cleaned_data['upload_file'].name
|
||||||
|
if isinstance(storage, ScriptFileSystemStorage):
|
||||||
|
full_path = os.path.join(settings.SCRIPTS_ROOT, filename)
|
||||||
|
else:
|
||||||
|
full_path = filename
|
||||||
|
|
||||||
|
self.instance.file_path = full_path
|
||||||
|
data = self.cleaned_data['upload_file']
|
||||||
|
storage.save(filename, data)
|
||||||
|
|
||||||
|
# need to skip ManagedFileForm save method
|
||||||
|
return super(ManagedFileForm, self).save(*args, **kwargs)
|
||||||
|
@ -1,11 +1,31 @@
|
|||||||
|
import importlib.abc
|
||||||
|
import importlib.util
|
||||||
import os
|
import os
|
||||||
from importlib.machinery import SourceFileLoader
|
import sys
|
||||||
|
from django.core.files.storage import storages
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'PythonModuleMixin',
|
'PythonModuleMixin',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomStoragesLoader(importlib.abc.Loader):
|
||||||
|
"""
|
||||||
|
Custom loader for exec_module to use django-storages instead of the file system.
|
||||||
|
"""
|
||||||
|
def __init__(self, filename):
|
||||||
|
self.filename = filename
|
||||||
|
|
||||||
|
def create_module(self, spec):
|
||||||
|
return None # Use default module creation
|
||||||
|
|
||||||
|
def exec_module(self, module):
|
||||||
|
storage = storages.create_storage(storages.backends["scripts"])
|
||||||
|
with storage.open(self.filename, 'rb') as f:
|
||||||
|
code = f.read()
|
||||||
|
exec(code, module.__dict__)
|
||||||
|
|
||||||
|
|
||||||
class PythonModuleMixin:
|
class PythonModuleMixin:
|
||||||
|
|
||||||
def get_jobs(self, name):
|
def get_jobs(self, name):
|
||||||
@ -33,6 +53,16 @@ class PythonModuleMixin:
|
|||||||
return name
|
return name
|
||||||
|
|
||||||
def get_module(self):
|
def get_module(self):
|
||||||
loader = SourceFileLoader(self.python_name, self.full_path)
|
"""
|
||||||
module = loader.load_module()
|
Load the module using importlib, but use a custom loader to use django-storages
|
||||||
|
instead of the file system.
|
||||||
|
"""
|
||||||
|
spec = importlib.util.spec_from_file_location(self.python_name, self.name)
|
||||||
|
if spec is None:
|
||||||
|
raise ModuleNotFoundError(f"Could not find module: {self.python_name}")
|
||||||
|
loader = CustomStoragesLoader(self.name)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[self.python_name] = module
|
||||||
|
loader.exec_module(module)
|
||||||
|
|
||||||
return module
|
return module
|
||||||
|
@ -2,10 +2,12 @@ import inspect
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.files.storage import storages
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import classproperty
|
from django.utils.functional import classproperty
|
||||||
@ -367,9 +369,46 @@ class BaseScript:
|
|||||||
def filename(self):
|
def filename(self):
|
||||||
return inspect.getfile(self.__class__)
|
return inspect.getfile(self.__class__)
|
||||||
|
|
||||||
|
def findsource(self, object):
|
||||||
|
storage = storages.create_storage(storages.backends["scripts"])
|
||||||
|
with storage.open(os.path.basename(self.filename), 'r') as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
# Break the source code into lines
|
||||||
|
lines = [line + '\n' for line in data.splitlines()]
|
||||||
|
|
||||||
|
# Find the class definition
|
||||||
|
name = object.__name__
|
||||||
|
pat = re.compile(r'^(\s*)class\s*' + name + r'\b')
|
||||||
|
# use the class definition with the least indentation
|
||||||
|
candidates = []
|
||||||
|
for i in range(len(lines)):
|
||||||
|
match = pat.match(lines[i])
|
||||||
|
if match:
|
||||||
|
if lines[i][0] == 'c':
|
||||||
|
return lines, i
|
||||||
|
|
||||||
|
candidates.append((match.group(1), i))
|
||||||
|
if not candidates:
|
||||||
|
raise OSError('could not find class definition')
|
||||||
|
|
||||||
|
# Sort the candidates by whitespace, and by line number
|
||||||
|
candidates.sort()
|
||||||
|
return lines, candidates[0][1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source(self):
|
def source(self):
|
||||||
return inspect.getsource(self.__class__)
|
# Can't use inspect.getsource() as it uses os to get the file
|
||||||
|
# inspect uses ast, but that is overkill for this as we only do
|
||||||
|
# classes.
|
||||||
|
object = self.__class__
|
||||||
|
|
||||||
|
try:
|
||||||
|
lines, lnum = self.findsource(object)
|
||||||
|
lines = inspect.getblock(lines[lnum:])
|
||||||
|
return ''.join(lines)
|
||||||
|
except OSError:
|
||||||
|
return ''
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_vars(cls):
|
def _get_vars(cls):
|
||||||
@ -524,7 +563,12 @@ class BaseScript:
|
|||||||
def load_yaml(self, filename):
|
def load_yaml(self, filename):
|
||||||
"""
|
"""
|
||||||
Return data from a YAML file
|
Return data from a YAML file
|
||||||
|
TODO: DEPRECATED: Remove this method in v4.4
|
||||||
"""
|
"""
|
||||||
|
self._log(
|
||||||
|
_("load_yaml is deprecated and will be removed in v4.4"),
|
||||||
|
level=LogLevelChoices.LOG_WARNING
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
from yaml import CLoader as Loader
|
from yaml import CLoader as Loader
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -539,7 +583,12 @@ class BaseScript:
|
|||||||
def load_json(self, filename):
|
def load_json(self, filename):
|
||||||
"""
|
"""
|
||||||
Return data from a JSON file
|
Return data from a JSON file
|
||||||
|
TODO: DEPRECATED: Remove this method in v4.4
|
||||||
"""
|
"""
|
||||||
|
self._log(
|
||||||
|
_("load_json is deprecated and will be removed in v4.4"),
|
||||||
|
level=LogLevelChoices.LOG_WARNING
|
||||||
|
)
|
||||||
file_path = os.path.join(settings.SCRIPTS_ROOT, filename)
|
file_path = os.path.join(settings.SCRIPTS_ROOT, filename)
|
||||||
with open(file_path, 'r') as datafile:
|
with open(file_path, 'r') as datafile:
|
||||||
data = json.load(datafile)
|
data = json.load(datafile)
|
||||||
@ -555,7 +604,6 @@ class BaseScript:
|
|||||||
Run the report and save its results. Each test method will be executed in order.
|
Run the report and save its results. Each test method will be executed in order.
|
||||||
"""
|
"""
|
||||||
self.logger.info("Running report")
|
self.logger.info("Running report")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for test_name in self.tests:
|
for test_name in self.tests:
|
||||||
self._current_test = test_name
|
self._current_test = test_name
|
||||||
|
14
netbox/extras/storage.py
Normal file
14
netbox/extras/storage.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
|
||||||
|
class ScriptFileSystemStorage(FileSystemStorage):
|
||||||
|
"""
|
||||||
|
Custom storage for scripts - for django-storages as the default one will
|
||||||
|
go off media-root and raise security errors as the scripts can be outside
|
||||||
|
the media-root directory.
|
||||||
|
"""
|
||||||
|
@cached_property
|
||||||
|
def base_location(self):
|
||||||
|
return settings.SCRIPTS_ROOT
|
@ -12,7 +12,6 @@ from django.utils.translation import gettext as _
|
|||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from core.choices import ManagedFileRootPathChoices
|
from core.choices import ManagedFileRootPathChoices
|
||||||
from core.forms import ManagedFileForm
|
|
||||||
from core.models import Job
|
from core.models import Job
|
||||||
from core.tables import JobTable
|
from core.tables import JobTable
|
||||||
from dcim.models import Device, DeviceRole, Platform
|
from dcim.models import Device, DeviceRole, Platform
|
||||||
@ -1163,7 +1162,7 @@ class DashboardWidgetDeleteView(LoginRequiredMixin, View):
|
|||||||
@register_model_view(ScriptModule, 'edit')
|
@register_model_view(ScriptModule, 'edit')
|
||||||
class ScriptModuleCreateView(generic.ObjectEditView):
|
class ScriptModuleCreateView(generic.ObjectEditView):
|
||||||
queryset = ScriptModule.objects.all()
|
queryset = ScriptModule.objects.all()
|
||||||
form = ManagedFileForm
|
form = forms.ScriptFileForm
|
||||||
|
|
||||||
def alter_object(self, obj, *args, **kwargs):
|
def alter_object(self, obj, *args, **kwargs):
|
||||||
obj.file_root = ManagedFileRootPathChoices.SCRIPTS
|
obj.file_root = ManagedFileRootPathChoices.SCRIPTS
|
||||||
|
@ -17,6 +17,7 @@ from netbox.config import PARAMS as CONFIG_PARAMS
|
|||||||
from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
||||||
from netbox.plugins import PluginConfig
|
from netbox.plugins import PluginConfig
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
|
import storages.utils # type: ignore
|
||||||
from utilities.release import load_release_data
|
from utilities.release import load_release_data
|
||||||
from utilities.string import trailing_slash
|
from utilities.string import trailing_slash
|
||||||
|
|
||||||
@ -177,7 +178,8 @@ SESSION_COOKIE_PATH = CSRF_COOKIE_PATH
|
|||||||
SESSION_COOKIE_SECURE = getattr(configuration, 'SESSION_COOKIE_SECURE', False)
|
SESSION_COOKIE_SECURE = getattr(configuration, 'SESSION_COOKIE_SECURE', False)
|
||||||
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
|
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
|
||||||
STORAGE_BACKEND = getattr(configuration, 'STORAGE_BACKEND', None)
|
STORAGE_BACKEND = getattr(configuration, 'STORAGE_BACKEND', None)
|
||||||
STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', {})
|
STORAGE_CONFIG = getattr(configuration, 'STORAGE_CONFIG', None)
|
||||||
|
STORAGES = getattr(configuration, 'STORAGES', {})
|
||||||
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
|
TIME_ZONE = getattr(configuration, 'TIME_ZONE', 'UTC')
|
||||||
TRANSLATION_ENABLED = getattr(configuration, 'TRANSLATION_ENABLED', True)
|
TRANSLATION_ENABLED = getattr(configuration, 'TRANSLATION_ENABLED', True)
|
||||||
|
|
||||||
@ -234,40 +236,49 @@ DATABASES = {
|
|||||||
# Storage backend
|
# Storage backend
|
||||||
#
|
#
|
||||||
|
|
||||||
|
if STORAGE_BACKEND is not None:
|
||||||
|
if not STORAGES:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"STORAGE_BACKEND and STORAGES are both set, remove the deprecated STORAGE_BACKEND setting."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
warnings.warn(
|
||||||
|
"STORAGE_BACKEND is deprecated, use the new STORAGES setting instead."
|
||||||
|
)
|
||||||
|
|
||||||
|
if STORAGE_CONFIG is not None:
|
||||||
|
warnings.warn(
|
||||||
|
"STORAGE_CONFIG is deprecated, use the new STORAGES setting instead."
|
||||||
|
)
|
||||||
|
|
||||||
# Default STORAGES for Django
|
# Default STORAGES for Django
|
||||||
STORAGES = {
|
DEFAULT_STORAGES = {
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||||
},
|
},
|
||||||
"staticfiles": {
|
"staticfiles": {
|
||||||
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"BACKEND": "extras.storage.ScriptFileSystemStorage",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
STORAGES = DEFAULT_STORAGES | STORAGES
|
||||||
|
|
||||||
|
# TODO: This code is deprecated and needs to be removed in the future
|
||||||
if STORAGE_BACKEND is not None:
|
if STORAGE_BACKEND is not None:
|
||||||
STORAGES['default']['BACKEND'] = STORAGE_BACKEND
|
STORAGES['default']['BACKEND'] = STORAGE_BACKEND
|
||||||
|
|
||||||
# django-storages
|
# Monkey-patch django-storages to fetch settings from STORAGE_CONFIG
|
||||||
if STORAGE_BACKEND.startswith('storages.'):
|
if STORAGE_CONFIG is not None:
|
||||||
try:
|
|
||||||
import storages.utils # type: ignore
|
|
||||||
except ModuleNotFoundError as e:
|
|
||||||
if getattr(e, 'name') == 'storages':
|
|
||||||
raise ImproperlyConfigured(
|
|
||||||
f"STORAGE_BACKEND is set to {STORAGE_BACKEND} but django-storages is not present. It can be "
|
|
||||||
f"installed by running 'pip install django-storages'."
|
|
||||||
)
|
|
||||||
raise e
|
|
||||||
|
|
||||||
# Monkey-patch django-storages to fetch settings from STORAGE_CONFIG
|
|
||||||
def _setting(name, default=None):
|
def _setting(name, default=None):
|
||||||
if name in STORAGE_CONFIG:
|
if name in STORAGE_CONFIG:
|
||||||
return STORAGE_CONFIG[name]
|
return STORAGE_CONFIG[name]
|
||||||
return globals().get(name, default)
|
return globals().get(name, default)
|
||||||
storages.utils.setting = _setting
|
storages.utils.setting = _setting
|
||||||
|
|
||||||
# django-storage-swift
|
# django-storage-swift
|
||||||
elif STORAGE_BACKEND == 'swift.storage.SwiftStorage':
|
if STORAGE_BACKEND == 'swift.storage.SwiftStorage':
|
||||||
try:
|
try:
|
||||||
import swift.utils # noqa: F401
|
import swift.utils # noqa: F401
|
||||||
except ModuleNotFoundError as e:
|
except ModuleNotFoundError as e:
|
||||||
@ -282,13 +293,7 @@ if STORAGE_BACKEND is not None:
|
|||||||
for param, value in STORAGE_CONFIG.items():
|
for param, value in STORAGE_CONFIG.items():
|
||||||
if param.startswith('SWIFT_'):
|
if param.startswith('SWIFT_'):
|
||||||
globals()[param] = value
|
globals()[param] = value
|
||||||
|
# TODO: End of deprecated code
|
||||||
if STORAGE_CONFIG and STORAGE_BACKEND is None:
|
|
||||||
warnings.warn(
|
|
||||||
"STORAGE_CONFIG has been set in configuration.py but STORAGE_BACKEND is not defined. STORAGE_CONFIG will be "
|
|
||||||
"ignored."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Redis
|
# Redis
|
||||||
|
@ -10,6 +10,7 @@ django-prometheus==2.3.1
|
|||||||
django-redis==5.4.0
|
django-redis==5.4.0
|
||||||
django-rich==1.13.0
|
django-rich==1.13.0
|
||||||
django-rq==3.0
|
django-rq==3.0
|
||||||
|
django-storages==1.14.4
|
||||||
django-taggit==6.1.0
|
django-taggit==6.1.0
|
||||||
django-tables2==2.7.5
|
django-tables2==2.7.5
|
||||||
django-timezone-field==7.1
|
django-timezone-field==7.1
|
||||||
|
Loading…
Reference in New Issue
Block a user