From 3f6046fed690093231e27812f8e4eabb52f3d356 Mon Sep 17 00:00:00 2001 From: Tris Date: Fri, 12 Mar 2021 11:08:40 +1100 Subject: [PATCH] Initial commit --- CHANGELOG.rst | 2 ++ README.rst | 22 ++++++++++++ byostorage/__init__.py | 1 + byostorage/admin.py | 7 ++++ byostorage/apps.py | 6 ++++ byostorage/migrations/0001_initial.py | 27 ++++++++++++++ byostorage/migrations/__init__.py | 0 byostorage/models.py | 18 ++++++++++ byostorage/storage.py | 52 +++++++++++++++++++++++++++ setup.cfg | 35 ++++++++++++++++++ setup.py | 3 ++ 11 files changed, 173 insertions(+) create mode 100644 CHANGELOG.rst create mode 100644 README.rst create mode 100644 byostorage/__init__.py create mode 100644 byostorage/admin.py create mode 100644 byostorage/apps.py create mode 100644 byostorage/migrations/0001_initial.py create mode 100644 byostorage/migrations/__init__.py create mode 100644 byostorage/models.py create mode 100644 byostorage/storage.py create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..85ec707 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,2 @@ +django-byostorage CHANGELOG +=========================== diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..9758919 --- /dev/null +++ b/README.rst @@ -0,0 +1,22 @@ +Installation +============ + +Usage +===== + +.. code-block:: python + + def generate_filename(instance, filename): + return f"{instance.parent.storage}:some_folder/{filename}" + + class SomeParentModel(models.Model): + storage = models.ForeignKey('byostorage.UserStorage', + on_delete=models.CASCADE) + + class MyModel(models.Model): + parent = models.ForeignKey('SomeParentModel', + on_delete=models.CASCADE) + + photo = models.FileField( + storage=BYOStorage(), + upload_to=generate_filename) diff --git a/byostorage/__init__.py b/byostorage/__init__.py new file mode 100644 index 0000000..f61ffe9 --- /dev/null +++ b/byostorage/__init__.py @@ -0,0 +1 @@ +__VERSION__ = 0.1 diff --git a/byostorage/admin.py b/byostorage/admin.py new file mode 100644 index 0000000..e628d56 --- /dev/null +++ b/byostorage/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from . import models + +class UserStorageAdmin(admin.ModelAdmin): + list_display = ['name', 'storage', 'owner'] + +admin.site.register(models.UserStorage, UserStorageAdmin) \ No newline at end of file diff --git a/byostorage/apps.py b/byostorage/apps.py new file mode 100644 index 0000000..ec16a1f --- /dev/null +++ b/byostorage/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ByostorageConfig(AppConfig): + name = 'byostorage' + verbose_name = 'Bring your own storage' diff --git a/byostorage/migrations/0001_initial.py b/byostorage/migrations/0001_initial.py new file mode 100644 index 0000000..13406a4 --- /dev/null +++ b/byostorage/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 3.1.1 on 2021-03-11 23:11 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='UserStorage', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.SlugField(help_text='Storage tag', max_length=20)), + ('storage', models.CharField(help_text='Storage class for this instance', max_length=255)), + ('settings_data', models.TextField(help_text='JSON dict with key/value settings')), + ('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/byostorage/migrations/__init__.py b/byostorage/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/byostorage/models.py b/byostorage/models.py new file mode 100644 index 0000000..6a1e157 --- /dev/null +++ b/byostorage/models.py @@ -0,0 +1,18 @@ +from django.db import models +from django.core.files.storage import get_storage_class +import json + +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") + settings_data = models.TextField(help_text="JSON dict with key/value settings") + + def instance(self): + return get_storage_class(self.storage)(**self.settings) + + @property + def settings(self): + return json.loads(self.settings_data) \ No newline at end of file diff --git a/byostorage/storage.py b/byostorage/storage.py new file mode 100644 index 0000000..4315895 --- /dev/null +++ b/byostorage/storage.py @@ -0,0 +1,52 @@ +from django.core.files.storage import Storage + +class MultiStorage(Storage): + ''' Django storage class that proxies multiple storage classes. + + Uses a name prefix to determine store e.g. 'images:foo/bar.jpg' + ''' + + def __init__(self, config=None): + self.config = config or getattr(settings, 'BRING_YOUR_OWN_STORAGE', {}) + self._cache = {} + + def get_storage(self, name): + if name not in self._cache: + config = self.config[name] + self._cache[name] = get_storage_class(config['storage'])(**config['settings']) + 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) + + def _proxy(self, 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}" + + + def _save(self, name, content, max_length=None): + return self._proxy('save', name, content, max_length) + +class BYOStorage(MultiStorage): + ''' Database driven Bring-Your-Own-Storage + + Multiple storages can + ''' + + def get_storage(self, name): + if name not in self._cache: + + # use storage from config by default + if name in self.config: + return super(BYOStorage, self).get_storage(name) + + from .models import UserStorage + obj = UserStorage.objects.get(name=name) + self._cache[name] = obj.instance() + return self._cache[name] \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..61ea38c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,35 @@ +[metadata] +name = django-byostorage +version = attr: byostorage.__version__ +description = Support for Bring-Your-Own-Storage +long_description = file: README.rst, CHANGELOG.rst +license = BSD-3-Clause +author = Tris Forster +author_email = tris.forster@gmail.com +url = https://github.com/tf198/django-byostorage +classifiers = + Development Status :: 1 - Development + Environment :: Web Environment + Framework :: Django + Framework :: Django :: 2.2 + Framework :: Django :: 3.0 + Framework :: Django :: 3.1 + Intended Audience :: Developers + License :: OSI Approved :: BSD License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + +[options] +python_requires = >=3.5 +install_requires = + Django >= 2.2 +packages = + byostorage + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6068493 --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup()