diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d35cb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +*.pyc diff --git a/byostorage/__init__.py b/byostorage/__init__.py index f61ffe9..1460570 100644 --- a/byostorage/__init__.py +++ b/byostorage/__init__.py @@ -1 +1,2 @@ -__VERSION__ = 0.1 +__version__ = 0.1 +default_app_config = 'byostorage.apps.BYOStorageConfig' \ No newline at end of file diff --git a/byostorage/apps.py b/byostorage/apps.py index ec16a1f..8a67a34 100644 --- a/byostorage/apps.py +++ b/byostorage/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig -class ByostorageConfig(AppConfig): +class BYOStorageConfig(AppConfig): name = 'byostorage' verbose_name = 'Bring your own storage' diff --git a/byostorage/local_settings.py b/byostorage/local_settings.py new file mode 100644 index 0000000..e963dfd --- /dev/null +++ b/byostorage/local_settings.py @@ -0,0 +1,6 @@ +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'byostorage' +] +SECRET_KEY = 'shh!' \ No newline at end of file diff --git a/byostorage/migrations/0002_auto_20210311_1826.py b/byostorage/migrations/0002_auto_20210311_1826.py new file mode 100644 index 0000000..59c864b --- /dev/null +++ b/byostorage/migrations/0002_auto_20210311_1826.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.7 on 2021-03-11 18:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('byostorage', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='userstorage', + name='storage', + field=models.CharField(choices=[], help_text='Storage class for this instance', max_length=255), + ), + ] diff --git a/byostorage/migrations/0003_auto_20210311_1917.py b/byostorage/migrations/0003_auto_20210311_1917.py new file mode 100644 index 0000000..e38a1dc --- /dev/null +++ b/byostorage/migrations/0003_auto_20210311_1917.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.7 on 2021-03-11 19:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('byostorage', '0002_auto_20210311_1826'), + ] + + operations = [ + migrations.AlterField( + model_name='userstorage', + name='name', + field=models.SlugField(help_text='Storage tag', max_length=20, unique=True), + ), + migrations.AlterField( + model_name='userstorage', + name='storage', + field=models.CharField(choices=[('django.core.files.storage.FileSystemStorage', 'django.core.files.storage.FileSystemStorage')], help_text='Storage class for this instance', max_length=255), + ), + ] diff --git a/byostorage/models.py b/byostorage/models.py index 6a1e157..9de8530 100644 --- a/byostorage/models.py +++ b/byostorage/models.py @@ -1,13 +1,34 @@ +from django.conf import settings from django.db import models from django.core.files.storage import get_storage_class import json +STORAGE_CLASSES = getattr(settings, 'STORAGE_CLASSES', []) + +STORAGE_CLASSES.extend([ + 'django.core.files.storage.FileSystemStorage', +]) + +try: + import storages + STORAGE_CLASSES.append('storages.backends.s3boto3.S3Boto3Storage') +except ImportError: + pass + +STORAGE_CLASS_OPTIONS = [ (x, x) for x in STORAGE_CLASSES ] + 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, help_text="Storage tag") - storage = models.CharField(max_length=255, help_text="Storage class for this instance") + owner = models.ForeignKey('auth.User', + on_delete=models.CASCADE, + null=True, blank=True) + name = models.SlugField(max_length=20, + unique=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(help_text="JSON dict with key/value settings") def instance(self): @@ -15,4 +36,14 @@ class UserStorage(models.Model): @property def settings(self): - return json.loads(self.settings_data) \ No newline at end of file + if not self.settings_data: + return {} + return json.loads(self.settings_data) + + def save(self, *args, **kwargs): + # check the settings are valid + self.settings + super(UserStorage, self).save(*args, **kwargs) + + def __str__(self): + return self.name \ No newline at end of file diff --git a/byostorage/storage.py b/byostorage/storage.py index 4315895..755eb51 100644 --- a/byostorage/storage.py +++ b/byostorage/storage.py @@ -1,4 +1,9 @@ from django.core.files.storage import Storage +from django.conf import settings + +''' +TODO: Create a signal to remove instances from cache if modified +''' class MultiStorage(Storage): ''' Django storage class that proxies multiple storage classes. @@ -17,21 +22,51 @@ class MultiStorage(Storage): return self._cache[name] def split(self, name): - return name.split(":", 1) - - def _open(self, name, mode='rb'): - storage, p = self.split(name) - return self.get_storage(storage)._open(p, mode) + return name.split("/", 1) def _proxy(self, method, name, *args, **kwargs): + print('PROXY', method, name, *args, **kwargs) storage, p = self.split(name) - sname = getattr(self.get_storage(storage), method)(name, *args, **kwargs) - if sname is not None: - return f"{storage}:{sname}" + result = getattr(self.get_storage(storage), method)(p, *args, **kwargs) + print("RESULT", result) + return result + def _proxy_name(self, method, name, *args, **kwargs): + print('PROXY_NAME', method, name, *args, **kwargs) + storage, p = self.split(name) + result = getattr(self.get_storage(storage), method)(p, *args, **kwargs) + print("RESULT", result) + return f"{storage}/{result}" + + def _open(self, name, mode='rb'): + return self._proxy('_open', name, mode) def _save(self, name, content, max_length=None): - return self._proxy('save', name, content, max_length) + return self._proxy_name('_save', name, content, max_length) + + def exists(self, name): + return self._proxy('exists', name) + + def delete(self, name): + return self._proxy('delete', name) + + def url(self, name): + return self._proxy('url', name) + + def get_accessed_time(self, name): + return self._proxy('get_accessed_time', name) + + def get_alternative_name(self, file_root, file_ext): + return self._proxy_name('get_alternative_name', file_root, file_ext) + + def get_available_name(self, name, max_length=None): + return self._proxy_name('get_available_name', name, max_length) + + def get_modified_time(self, name): + return self._proxy('get_modified_time', name) + + def get_valid_name(self, name): + return self._proxy_name('get_valid_name', name) class BYOStorage(MultiStorage): ''' Database driven Bring-Your-Own-Storage diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..855ebe8 --- /dev/null +++ b/manage.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +import sys, os + + +INSTALLED_APPS = ['byostorage'] + +if __name__ == "__main__": + from django.core.management import execute_from_command_line + + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'byostorage.local_settings') + execute_from_command_line(sys.argv) \ No newline at end of file