from django.conf import settings from django.db import models from django.db.models.fields.files import FieldFile from django.core.exceptions import ObjectDoesNotExist from django.core.files.storage import Storage from django.core.exceptions import ValidationError from django.utils.module_loading import import_string import json import logging logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) STORAGE_CLASSES = getattr(settings, 'STORAGE_CLASSES', []) STORAGE_CLASSES.extend([ 'django.core.files.storage.FileSystemStorage', ]) try: import storages.backends STORAGE_CLASSES.extend([ 'storages.backends.s3boto3.S3Boto3Storage', 'storages.backends.azure_storage.AzureStorage', 'storages.backends.dropbox.DropBoxStorage', 'storages.backends.ftp.FTPStorage', 'storages.backends.gcloud.GoogleCloudStorage', 'storages.backends.sftpstorage.SFTPStorage', ]) except ImportError: pass STORAGE_CLASS_OPTIONS = [ (x, x) for x in STORAGE_CLASSES ] def validate_json(value): try: json.loads(value) except json.JSONDecodeError as e: raise ValidationError(e.msg) def resolve_instance(obj, relation): for p in relation.split('__'): obj = getattr(obj, p) if callable(obj): obj = obj() return obj class UserStorage(models.Model): """ A user defined storage """ owner = models.ForeignKey('auth.User', on_delete=models.CASCADE, null=True, blank=True) name = models.SlugField(max_length=20, primary_key=True, help_text="Storage tag") storage = models.CharField(max_length=255, choices=STORAGE_CLASS_OPTIONS, help_text="Storage class for this instance") settings_data = models.TextField( validators=[validate_json], default='{}', help_text="JSON dict with key/value settings") def instance(self): return import_string(self.storage)(**self.settings) @property def settings(self): if not self.settings_data: return {} try: return json.loads(self.settings_data) except Exception as e: raise ValueError("Error in settings for storage '{0}' [{1}]".format(self.name, e)) def clean(self): try: self.test_storage() except Exception as e: raise ValidationError(str(e)) def test_storage(self): # just do something that requires connection try: self.instance().listdir('') except FileNotFoundError: # FileSystemStorage doesn't create the base_dir until write pass return True def save(self, *args, **kwargs): # ensure the settings JSON isvalid if self.settings_data: validate_json(self.settings_data) super(UserStorage, self).save(*args, **kwargs) def __str__(self): return self.name class BYOFieldFile(FieldFile): def __init__(self, instance, field, name): logger.debug("BYOFieldFile(%r, %r, %r)", instance, field, name) super().__init__(instance, field, name) try: self.storage = field.get_storage(instance) logger.debug("Using BYO storage: %r", self.storage) except ObjectDoesNotExist: logger.debug("Unable to select BYO storage") self.storage = None # trigger error if try to save etc class BYOStorageField(models.FileField): attr_class = BYOFieldFile def __init__(self, storage_instance, *args, **kwargs): self.storage_instance = storage_instance super().__init__(*args, **kwargs) def deconstruct(self): name, path, args, kwargs = super().deconstruct() args = [self.storage_instance] + args return name, path, args, kwargs def get_storage(self, instance): if callable(self.storage_instance): storage = self.storage_instance(instance) else: storage = resolve_instance(instance, self.storage_instance) if not isinstance(storage, Storage): raise RuntimeError("Not a storage instance") return storage #def formfield(self, **kwargs): # return BYOStorageFormField(**kwargs)