Skip to content

Commit

Permalink
Provide external file storage options for uploaded files
Browse files Browse the repository at this point in the history
- Provide options in configuration.py to allow external modules to be loaded to provide alternative methods for file upload/download.
- Upload alternative is implemented as a Django Storage class.
- Download alternative is implemented as a Django app.

Signed-off-by: Nathan Ward <[email protected]>
  • Loading branch information
nward committed Apr 20, 2019
1 parent 8b75969 commit 074ace6
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 6 deletions.
52 changes: 52 additions & 0 deletions docs/configuration/optional-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,3 +322,55 @@ The password to use when authenticating to the Redis server (optional).
Default: False

Use secure sockets layer to encrypt the connections to the Redis server.

## Custom Storage

Custom storage classes can be used to store uploaded files (i.e. image attachments) in places other than the filesystem.
This is useful, for example, if you have a need to store files in object storage services like S3, or in a database of some kind.

You can optionally define an application which is used to serve the files. If this is not provided, the files are expected to be served externally, and access control is not provided by NetBox.

### FILE_STORAGE

Default: None

The storage class. Populates the `DEFAULT_FILE_STORAGE` setting. See [Writing a custom storage system](https://docs.djangoproject.com/en/2.2/howto/custom-file-storage/) for more information on what this needs to provide.

### FILE_STORAGE_APP

Default: None

A Django app to load, in order to view/download files uploaded with `FILE_STORAGE` above.

### FILE_STORAGE_APP_CONFIG

Default: None

A dictionary of config parameters which are set in the settings.py of the main Django application. For example, AWS credentials for S3 in the format required by `django-storages` can be set as follows:

```
FILE_STORAGE_APP_CONFIG = {
'AWS_ACCESS_KEY_ID': 'foo',
'AWS_SECRET_ACCESS_KEY': 'bar',
...
etc.
}
```

### FILE_STORAGE_APP_PREFIX

Default: media

The URL prefix for the above app. Defaults to `media` - which is what NetBox requires for if using the default filesystem storage mechanism.

### FILE_STORAGE_UPLOAD_TO

Default: None

A function name which is provided to the upload_to parameter of ImageField. See [FileField.upload_to](https://docs.djangoproject.com/en/2.2/ref/models/fields/#django.db.models.FileField.upload_to) for more information.

This is generally not required, however, some existing storage plugins require this to be in a specific format.

This is a string, of the form `module_a.module_b.function`. When required, the module is automatically imported, then the function is called.

NetBox requires the resulting name to be of the format `(.*/)?<type>_<id>_<filename>`, where `type` is `instance.content_type.name` (i.e. rack, device, etc.), and `id` is `instance.object_id`. See `netbox.extras.models.image_upload` for an example of this.
18 changes: 15 additions & 3 deletions netbox/extras/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
from django.http import HttpResponse
from django.template import Template, Context
from django.urls import reverse
from django.conf import settings
import graphviz
from jinja2 import Environment
import importlib

from dcim.constants import CONNECTION_STATUS_CONNECTED
from utilities.utils import deepmerge, foreground_color
Expand Down Expand Up @@ -574,7 +576,6 @@ def add_power_connections(self, devices):
#

def image_upload(instance, filename):

path = 'image-attachments/'

# Rename the file to the provided name, if any. Attempt to preserve the file extension.
Expand All @@ -591,6 +592,13 @@ class ImageAttachment(models.Model):
"""
An uploaded image which is associated with an object.
"""
if settings.FILE_STORAGE_UPLOAD_TO:
module_name, function_name = settings.FILE_STORAGE_UPLOAD_TO.rsplit('.', 1)
module = importlib.import_module(module_name)
upload_to = getattr(module, function_name)
else:
upload_to = image_upload

content_type = models.ForeignKey(
to=ContentType,
on_delete=models.CASCADE
Expand All @@ -601,7 +609,7 @@ class ImageAttachment(models.Model):
fk_field='object_id'
)
image = models.ImageField(
upload_to=image_upload,
upload_to=upload_to,
height_field='image_height',
width_field='image_width'
)
Expand All @@ -621,7 +629,11 @@ class Meta:
def __str__(self):
if self.name:
return self.name
filename = self.image.name.rsplit('/', 1)[-1]

filename = self.image.name
if '/' in filename:
filename = filename.rsplit('/', 1)[-1]

return filename.split('_', 2)[2]

def delete(self, *args, **kwargs):
Expand Down
17 changes: 17 additions & 0 deletions netbox/netbox/configuration.example.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,20 @@
SHORT_TIME_FORMAT = 'H:i:s'
DATETIME_FORMAT = 'N j, Y g:i a'
SHORT_DATETIME_FORMAT = 'Y-m-d H:i'

# Optional alternative file upload functionality

# Class to use for uploaded file storage
# FILE_STORAGE = 'custom_storage.storage.Storage'

# Django application for uploaded file storage retrieval
# FILE_STORAGE_APP = 'custom_storage'

# Custom config settings required by FILE_STORAGE_APP
# FILE_STORAGE_APP_CONFIG = {'PARAM': 'value'}

# Prefix under which the above app is accessed
# FILE_STORAGE_APP_PREFIX = 'files'

# Custom 'upload_to' function passed to ImageField/FileField
# FILE_STORAGE_UPLOAD_TO = 'custom_storage.upload_to'
24 changes: 21 additions & 3 deletions netbox/netbox/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,6 @@
os.path.join(BASE_DIR, "project-static"),
)

# Media
MEDIA_URL = '/{}media/'.format(BASE_PATH)

# Disable default limit of 1000 fields per request. Needed for bulk deletion of objects. (Added in Django 1.10.)
DATA_UPLOAD_MAX_NUMBER_FIELDS = None

Expand Down Expand Up @@ -340,6 +337,27 @@
'::1',
)

# Custom file upload handling
if hasattr(configuration, 'FILE_STORAGE'):
DEFAULT_FILE_STORAGE = getattr(configuration, 'FILE_STORAGE')

FILE_STORAGE_APP = getattr(configuration, 'FILE_STORAGE_APP', None)
if FILE_STORAGE_APP:
INSTALLED_APPS.append(FILE_STORAGE_APP)
FILE_STORAGE_APP_CONFIG = getattr(configuration, 'FILE_STORAGE_APP_CONFIG', {})
if FILE_STORAGE_APP_CONFIG:
for attr_name, attr_value in FILE_STORAGE_APP_CONFIG.items():
globals()[attr_name] = attr_value

# Default MEDIA_URL to /<BASE_PATH>media/, but allow FILE_STORAGE_APP_PREFIX to override it only if FILE_STORAGE_APP is set
if FILE_STORAGE_APP:
FILE_STORAGE_APP_PREFIX = getattr(configuration, 'FILE_STORAGE_APP_PREFIX', 'media')
else:
FILE_STORAGE_APP_PREFIX = 'media'

MEDIA_URL = '/{}{}/'.format(BASE_PATH, FILE_STORAGE_APP_PREFIX)

FILE_STORAGE_UPLOAD_TO = getattr(configuration, 'FILE_STORAGE_UPLOAD_TO', None)

try:
HOSTNAME = socket.gethostname()
Expand Down
5 changes: 5 additions & 0 deletions netbox/netbox/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@
url(r'^__debug__/', include(debug_toolbar.urls)),
]

if settings.FILE_STORAGE_APP:
_patterns += [
url(r'^{}/'.format(settings.FILE_STORAGE_APP_PREFIX), include('{}.urls'.format(settings.FILE_STORAGE_APP))),
]

# Prepend BASE_PATH
urlpatterns = [
url(r'^{}'.format(settings.BASE_PATH), include(_patterns))
Expand Down

0 comments on commit 074ace6

Please sign in to comment.