Skip to content

Commit

Permalink
Merge pull request #26 from theriverman/develop
Browse files Browse the repository at this point in the history
Changes resolve #25
  • Loading branch information
theriverman authored Aug 1, 2021
2 parents 3fc7e22 + e144016 commit 14508a9
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 29 deletions.
10 changes: 9 additions & 1 deletion DjangoExampleApplication/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from django.db.models.query import QuerySet
from django.contrib import admin
from django.core.handlers.wsgi import WSGIRequest
from .models import PublicAttachment, PrivateAttachment, Image
from .models import PublicAttachment, PrivateAttachment, Image, GenericAttachment


# https://docs.djangoproject.com/en/2.2/ref/contrib/admin/actions/#writing-action-functions
Expand Down Expand Up @@ -32,6 +32,14 @@ class ImageAdmin(admin.ModelAdmin):
actions = [delete_everywhere, ]


@admin.register(GenericAttachment)
class GenericAttachmentAdmin(admin.ModelAdmin):
list_display = ('id', 'file',)
readonly_fields = ('id', )
model = GenericAttachment
actions = [delete_everywhere, ]


# Register your models here.
@admin.register(PublicAttachment)
class PublicAttachmentAdmin(admin.ModelAdmin):
Expand Down
21 changes: 21 additions & 0 deletions DjangoExampleApplication/migrations/0003_genericattachment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 3.2.3 on 2021-07-18 22:07

from django.db import migrations, models
import uuid


class Migration(migrations.Migration):

dependencies = [
('DjangoExampleApplication', '0002_auto_20210313_1049'),
]

operations = [
migrations.CreateModel(
name='GenericAttachment',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('file', models.FileField(upload_to='', verbose_name='Object Upload (to default storage)')),
],
),
]
19 changes: 18 additions & 1 deletion DjangoExampleApplication/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,30 @@ class Image(models.Model):

def delete(self, *args, **kwargs):
"""
Delete must be overridden because the inherited delete method does not call `self.file.delete()`.
Delete must be overridden because the inherited delete method does not call `self.image.delete()`.
"""
# noinspection PyUnresolvedReferences
self.image.delete()
super(Image, self).delete(*args, **kwargs)


class GenericAttachment(models.Model):
"""
This is for demonstrating uploads to the default file storage
"""
objects = models.Manager()
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
file = models.FileField(verbose_name="Object Upload (to default storage)")

def delete(self, *args, **kwargs):
"""
Delete must be overridden because the inherited delete method does not call `self.image.delete()`.
"""
# noinspection PyUnresolvedReferences
self.image.delete()
super(GenericAttachment, self).delete(*args, **kwargs)


# Create your models here.
class PublicAttachment(models.Model):
def set_file_path_name(self, file_name_ext: str) -> str:
Expand Down
11 changes: 9 additions & 2 deletions DjangoExampleProject/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_STORAGE = 'django_minio_backend.models.MinioBackendStatic'
DEFAULT_FILE_STORAGE = 'django_minio_backend.models.MinioBackend'

# #################### #
# django_minio_backend #
Expand Down Expand Up @@ -154,14 +156,19 @@
MINIO_USE_HTTPS = bool(distutils.util.strtobool(os.getenv("GH_MINIO_USE_HTTPS", "true")))
MINIO_PRIVATE_BUCKETS = [
'django-backend-dev-private',
'my-media-files-bucket',
]
MINIO_PUBLIC_BUCKETS = [
'django-backend-dev-public',
"t5p2g08k31",
"7xi7lx9rjh",
't5p2g08k31',
'7xi7lx9rjh',
'my-static-files-bucket',
]
MINIO_URL_EXPIRY_HOURS = timedelta(days=1) # Default is 7 days (longest) if not defined
MINIO_CONSISTENCY_CHECK_ON_START = True
MINIO_POLICY_HOOKS: List[Tuple[str, dict]] = [
# ('django-backend-dev-private', dummy_policy)
]
MINIO_MEDIA_FILES_BUCKET = 'my-media-files-bucket' # replacement for STATIC_ROOT
MINIO_STATIC_FILES_BUCKET = 'my-static-files-bucket' # replacement for MEDIA_ROOT
MINIO_BUCKET_CHECK_ON_SAVE = False # Create bucket if missing, then save
62 changes: 61 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ MINIO_PUBLIC_BUCKETS = [
'django-backend-dev-public',
]
MINIO_POLICY_HOOKS: List[Tuple[str, dict]] = []
# MINIO_MEDIA_FILES_BUCKET = 'my-media-files-bucket' # replacement for MEDIA_ROOT
# MINIO_STATIC_FILES_BUCKET = 'my-static-files-bucket' # replacement for STATIC_ROOT
MINIO_BUCKET_CHECK_ON_SAVE = True # Default: True // Creates bucket if missing, then save
```

4. Implement your own Attachment handler and integrate **django-minio-backend**:
Expand All @@ -79,6 +82,48 @@ python manage.py initialize_buckets

Code reference: [initialize_buckets.py](django_minio_backend/management/commands/initialize_buckets.py).

### Static Files Support
**django-minio-backend** allows serving static files from MinIO.
To learn more about Django static files, see [Managing static files](https://docs.djangoproject.com/en/3.2/howto/static-files/), and [STATICFILES_STORAGE](https://docs.djangoproject.com/en/3.2/ref/settings/#staticfiles-storage).

To enable static files support, update your `settings.py`:
```python
STATICFILES_STORAGE = 'django_minio_backend.models.MinioBackendStatic'
MINIO_STATIC_FILES_BUCKET = 'my-static-files-bucket' # replacement for STATIC_ROOT
# Add the value of MINIO_STATIC_FILES_BUCKET to one of the pre-configured bucket lists. eg.:
# MINIO_PRIVATE_BUCKETS.append(MINIO_STATIC_FILES_BUCKET)
# MINIO_PUBLIC_BUCKETS.append(MINIO_STATIC_FILES_BUCKET)
```

The value of `STATIC_URL` is ignored, but it must be defined otherwise Django will throw an error.

**IMPORTANT**<br>
The value set in `MINIO_STATIC_FILES_BUCKET` must be added either to `MINIO_PRIVATE_BUCKETS` or `MINIO_PUBLIC_BUCKETS`,
otherwise **django-minio-backend** will raise an exception. This setting determines the privacy of generated file URLs which can be unsigned public or signed private.

**Note:** If `MINIO_STATIC_FILES_BUCKET` is not set, the default value (`auto-generated-static-media-files`) will be used. Policy setting for default buckets is **private**.

### Default File Storage Support
**django-minio-backend** can be configured as a default file storage.
To learn more, see [DEFAULT_FILE_STORAGE](https://docs.djangoproject.com/en/3.2/ref/settings/#default-file-storage).

To configure **django-minio-backend** as the default file storage, update your `settings.py`:
```python
DEFAULT_FILE_STORAGE = 'django_minio_backend.models.MinioBackend'
MINIO_MEDIA_FILES_BUCKET = 'my-media-files-bucket' # replacement for MEDIA_ROOT
# Add the value of MINIO_STATIC_FILES_BUCKET to one of the pre-configured bucket lists. eg.:
# MINIO_PRIVATE_BUCKETS.append(MINIO_STATIC_FILES_BUCKET)
# MINIO_PUBLIC_BUCKETS.append(MINIO_STATIC_FILES_BUCKET)
```

The value of `MEDIA_URL` is ignored, but it must be defined otherwise Django will throw an error.

**IMPORTANT**<br>
The value set in `MINIO_MEDIA_FILES_BUCKET` must be added either to `MINIO_PRIVATE_BUCKETS` or `MINIO_PUBLIC_BUCKETS`,
otherwise **django-minio-backend** will raise an exception. This setting determines the privacy of generated file URLs which can be unsigned public or signed private.

**Note:** If `MINIO_MEDIA_FILES_BUCKET` is not set, the default value (`auto-generated-bucket-media-files`) will be used. Policy setting for default buckets is **private**.

### Health Check
To check the connection link between Django and MinIO, use the provided `MinioBackend.is_minio_available()` method.<br>
It returns a `MinioServerStatus` instance which can be quickly evaluated as boolean.<br>
Expand All @@ -87,7 +132,7 @@ It returns a `MinioServerStatus` instance which can be quickly evaluated as bool
```python
from django_minio_backend import MinioBackend

minio_available = MinioBackend('').is_minio_available() # An empty string is fine this time
minio_available = MinioBackend().is_minio_available() # An empty string is fine this time
if minio_available:
print("OK")
else:
Expand All @@ -112,6 +157,21 @@ In case a bucket is missing or its configuration differs, it gets created and co
### Reference Implementation
For a reference implementation, see [Examples](examples).

## Behaviour
The following list summarises the key characteristics of **django-minio-backend**:
* Bucket existence is **not** checked on save by default.
To enable this guard, set `MINIO_BUCKET_CHECK_ON_SAVE = True` in your `settings.py`.
* Bucket existences are **not** checked on Django start by default.
To enable this guard, set `MINIO_CONSISTENCY_CHECK_ON_START = True` in your `settings.py`.
* Many configuration errors are validated through `AppConfig` but not every error can be captured there.
* Files with the same name in the same bucket are **not** replaced on save by default. Django will store the newer file with an altered file name
To allow replacing existing files, pass the `replace_existing=True` kwarg to `MinioBackend`.
For example: `image = models.ImageField(storage=MinioBackend(bucket_name='images-public', replace_existing=True))`
* Depending on your configuration, **django-minio-backend** may communicate over two kind of interfaces: internal and external.
If your `settings.py` defines a different value for `MINIO_ENDPOINT` and `MINIO_EXTERNAL_ENDPOINT`, then the former will be used for internal communication
between Django and MinIO, and the latter for generating URLs for users. This behaviour optimises the network communication.
* The uploaded object's content-type is guessed during save. If `mimetypes.guess_type` fails to determine the correct content-type, then it falls back to `application/octet-stream`.

## Compatibility
* Django 2.2 or later
* Python 3.6.0 or later
Expand Down
7 changes: 7 additions & 0 deletions django_minio_backend/apps.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.apps import AppConfig
from .utils import get_setting, ConfigurationError
from .models import MinioBackendStatic


__all__ = ['DjangoMinioBackendConfig', ]
Expand All @@ -20,3 +21,9 @@ def ready(self):
external_use_https = get_setting('MINIO_EXTERNAL_ENDPOINT_USE_HTTPS')
if (external_address and external_use_https is None) or (not external_address and external_use_https):
raise ConfigurationError('MINIO_EXTERNAL_ENDPOINT must be configured together with MINIO_EXTERNAL_ENDPOINT_USE_HTTPS')

# Validate static storage and default storage configurations
staticfiles_storage: str = get_setting('STATICFILES_STORAGE')
if staticfiles_storage.endswith(MinioBackendStatic.__name__):
mbs = MinioBackendStatic()
mbs.check_bucket_existence()
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def handle(self, *args, **options):
self.stdout.write(
f"Bucket ({m.bucket}) policy has been set to public", ending='\n') if not silenced else None

c = MinioBackend('') # Client
c = MinioBackend() # Client
for policy_tuple in get_setting('MINIO_POLICY_HOOKS', []):
bucket, policy = policy_tuple
c.set_bucket_policy(bucket, policy)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ def add_arguments(self, parser):
parser.add_argument('--silenced', action='store_true', default=False, help='No console messages')

def handle(self, *args, **options):
m = MinioBackend('') # no configured bucket
m = MinioBackend() # use default storage
silenced = options.get('silenced')
self.stdout.write(f"Checking the availability of MinIO at {m.base_url}\n") if not silenced else None

Expand Down
Loading

0 comments on commit 14508a9

Please sign in to comment.