Compare commits
No commits in common. "9c9f1cb2bfc7972f828a1894ece507b3f4c627fa" and "2c858a03f2a9defd1ee5c6f261b02e3428c0fe36" have entirely different histories.
9c9f1cb2bf
...
2c858a03f2
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,3 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
*.pyc
|
*.pyc
|
||||||
django_byostorage.egg-info
|
django_byostorage.egg-info
|
||||||
.coverage
|
|
||||||
htmlcov
|
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
from django.core.files.storage import Storage
|
|
||||||
import requests
|
|
||||||
|
|
||||||
class HTTPStorage(Storage):
|
|
||||||
|
|
||||||
def __init__(self, protocol='https'):
|
|
||||||
self.protocol = protocol
|
|
||||||
|
|
||||||
def _open(self, name, mode='rb'):
|
|
||||||
if mode != 'rb':
|
|
||||||
raise IOError("Mode must be 'rb'")
|
|
||||||
|
|
||||||
r = requests.get(self.url(name), stream=True)
|
|
||||||
r.raise_for_status()
|
|
||||||
|
|
||||||
return r.raw
|
|
||||||
|
|
||||||
def _save(self, name, content):
|
|
||||||
raise NotImplementedError("Unable to save to web locations")
|
|
||||||
|
|
||||||
def url(self, name):
|
|
||||||
return f"{self.protocol}:{name}"
|
|
||||||
|
|
||||||
def exists(self, name):
|
|
||||||
r = requests.head(self.url(name))
|
|
||||||
return r.status_code == 200
|
|
||||||
@ -1,20 +1,6 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
||||||
|
|
||||||
MEDIA_ROOT = "media"
|
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
'byostorage'
|
'byostorage'
|
||||||
]
|
]
|
||||||
SECRET_KEY = 'shh!'
|
SECRET_KEY = 'shh!'
|
||||||
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
|
||||||
'NAME': BASE_DIR / 'db.sqlite3',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,6 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.core.files.storage import get_storage_class, FileSystemStorage
|
from django.core.files.storage import get_storage_class
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
STORAGE_CLASSES = getattr(settings, 'STORAGE_CLASSES', [])
|
STORAGE_CLASSES = getattr(settings, 'STORAGE_CLASSES', [])
|
||||||
@ -18,12 +17,6 @@ except ImportError:
|
|||||||
|
|
||||||
STORAGE_CLASS_OPTIONS = [ (x, x) for x in STORAGE_CLASSES ]
|
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)
|
|
||||||
|
|
||||||
class UserStorage(models.Model):
|
class UserStorage(models.Model):
|
||||||
""" A user defined storage
|
""" A user defined storage
|
||||||
"""
|
"""
|
||||||
@ -36,9 +29,7 @@ class UserStorage(models.Model):
|
|||||||
storage = models.CharField(max_length=255,
|
storage = models.CharField(max_length=255,
|
||||||
choices=STORAGE_CLASS_OPTIONS,
|
choices=STORAGE_CLASS_OPTIONS,
|
||||||
help_text="Storage class for this instance")
|
help_text="Storage class for this instance")
|
||||||
settings_data = models.TextField(
|
settings_data = models.TextField(help_text="JSON dict with key/value settings")
|
||||||
validators=[validate_json],
|
|
||||||
help_text="JSON dict with key/value settings")
|
|
||||||
|
|
||||||
def instance(self):
|
def instance(self):
|
||||||
return get_storage_class(self.storage)(**self.settings)
|
return get_storage_class(self.storage)(**self.settings)
|
||||||
@ -47,25 +38,11 @@ class UserStorage(models.Model):
|
|||||||
def settings(self):
|
def settings(self):
|
||||||
if not self.settings_data:
|
if not self.settings_data:
|
||||||
return {}
|
return {}
|
||||||
try:
|
return json.loads(self.settings_data)
|
||||||
return json.loads(self.settings_data)
|
|
||||||
except Exception as e:
|
|
||||||
raise ValueError("Error in settings for storage '{0}' [{1}]".format(self.name, 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):
|
def save(self, *args, **kwargs):
|
||||||
# ensure the settings JSON isvalid
|
# check the settings are valid
|
||||||
if self.settings_data:
|
self.settings
|
||||||
validate_json(self.settings_data)
|
|
||||||
|
|
||||||
super(UserStorage, self).save(*args, **kwargs)
|
super(UserStorage, self).save(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from django.core.files.storage import Storage, default_storage, get_storage_class
|
from django.core.files.storage import Storage
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.deconstruct import deconstructible
|
from django.utils.deconstruct import deconstructible
|
||||||
|
|
||||||
@ -24,12 +24,8 @@ class MultiStorage(Storage):
|
|||||||
|
|
||||||
def get_storage(self, name):
|
def get_storage(self, name):
|
||||||
if name not in self._cache:
|
if name not in self._cache:
|
||||||
try:
|
config = self.config[name]
|
||||||
config = self.config[name]
|
self._cache[name] = get_storage_class(config['storage'])(**config['settings'])
|
||||||
except KeyError:
|
|
||||||
return default_storage
|
|
||||||
klass = get_storage_class(config.pop('storage', None))
|
|
||||||
self._cache[name] = klass(**config)
|
|
||||||
return self._cache[name]
|
return self._cache[name]
|
||||||
|
|
||||||
def split(self, name):
|
def split(self, name):
|
||||||
@ -81,5 +77,21 @@ class MultiStorage(Storage):
|
|||||||
def get_modified_time(self, name):
|
def get_modified_time(self, name):
|
||||||
return self._proxy('get_modified_time', name)
|
return self._proxy('get_modified_time', name)
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "<MultiStorage: {0} storages>".format(len(self.config))
|
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]
|
||||||
@ -1,140 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
from django.core.files.storage import FileSystemStorage
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
|
|
||||||
from .multi import MultiStorage
|
|
||||||
from .models import UserStorage
|
|
||||||
from .user import BYOStorage
|
|
||||||
from .http import HTTPStorage
|
|
||||||
|
|
||||||
#import logging
|
|
||||||
#logging.basicConfig(level=logging.DEBUG)
|
|
||||||
|
|
||||||
class MultiStorageTestCase(TestCase):
|
|
||||||
|
|
||||||
def test_storage_selection(self):
|
|
||||||
ms = MultiStorage({'one': {'location': 'storage/one'}, 'two': {}})
|
|
||||||
self.assertEqual(str(ms), "<MultiStorage: 2 storages>")
|
|
||||||
|
|
||||||
# use the default if nothing set
|
|
||||||
default = ms.get_storage('foo')
|
|
||||||
self.assertIsInstance(default, FileSystemStorage)
|
|
||||||
self.assertEqual(default.base_location, 'media')
|
|
||||||
|
|
||||||
# get first instance
|
|
||||||
one = ms.get_storage('one')
|
|
||||||
self.assertEqual(one.base_location, 'storage/one')
|
|
||||||
|
|
||||||
def test_storage_proxies(self):
|
|
||||||
ms = MultiStorage({
|
|
||||||
'one': {'location': 'storage/one', 'base_url': 'http://one'},
|
|
||||||
'two': {'location': 'storage/two', 'base_url': 'http://two'}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
self.assertEqual(ms.url("one:foo.txt"), 'http://one/foo.txt')
|
|
||||||
self.assertEqual(ms.url("two:foo.txt"), 'http://two/foo.txt')
|
|
||||||
|
|
||||||
def test_with_http(self):
|
|
||||||
ms = MultiStorage({
|
|
||||||
'https': {'storage': 'byostorage.http.HTTPStorage', 'protocol': 'https'}
|
|
||||||
})
|
|
||||||
|
|
||||||
self.assertEqual(ms.url('https://google.com'), 'https://google.com')
|
|
||||||
|
|
||||||
class UserStorageTestCase(TestCase):
|
|
||||||
|
|
||||||
def test_construction(self):
|
|
||||||
us = UserStorage.objects.create(name='one', storage='django.core.files.storage.FileSystemStorage',
|
|
||||||
settings_data='{"location": "storage/one"}')
|
|
||||||
|
|
||||||
self.assertEqual(str(us), 'one')
|
|
||||||
|
|
||||||
storage = us.instance()
|
|
||||||
self.assertIsInstance(storage, FileSystemStorage)
|
|
||||||
self.assertEqual(storage.base_location, 'storage/one')
|
|
||||||
|
|
||||||
def test_bad_json(self):
|
|
||||||
with self.assertRaisesMessage(ValidationError, "Unterminated string"):
|
|
||||||
UserStorage.objects.create(name='one', storage='django.core.files.storage.FileSystemStorage',
|
|
||||||
settings_data='{"location": "foo}')
|
|
||||||
|
|
||||||
def test_connection(self):
|
|
||||||
instance = UserStorage.objects.create(name='one', storage='storages.backends.s3boto3.S3Boto3Storage',
|
|
||||||
settings_data='''
|
|
||||||
{
|
|
||||||
"access_key": "polyphonic_test_key",
|
|
||||||
"secret_key": "polyphonic_secret",
|
|
||||||
"endpoint_url": "http://localhost:9000",
|
|
||||||
"bucket_name": "personal"
|
|
||||||
}
|
|
||||||
''')
|
|
||||||
|
|
||||||
|
|
||||||
def test_bad_connection(self):
|
|
||||||
try:
|
|
||||||
UserStorage.objects.create(name='one', storage='storages.backends.s3boto3.S3Boto3Storage',
|
|
||||||
settings_data='''
|
|
||||||
{
|
|
||||||
"access_key": "polyphonic_test_key",
|
|
||||||
"secret_key": "the_wrong_secret",
|
|
||||||
"endpoint_url": "http://localhost:9000",
|
|
||||||
"bucket_name": "missing"
|
|
||||||
}
|
|
||||||
''')
|
|
||||||
self.fail("Should have raised an exception")
|
|
||||||
except Exception as e:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class BYOStorageTestCase(TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
UserStorage.objects.create(name='one', storage='django.core.files.storage.FileSystemStorage',
|
|
||||||
settings_data='{"location": "storage/one"}')
|
|
||||||
|
|
||||||
def test_pickup_changes(self):
|
|
||||||
storage = BYOStorage()
|
|
||||||
one = storage.get_storage('one')
|
|
||||||
self.assertEqual(one.base_location, 'storage/one')
|
|
||||||
|
|
||||||
# if not already in cache then no issues
|
|
||||||
UserStorage.objects.create(name='two', storage='django.core.files.storage.FileSystemStorage',
|
|
||||||
settings_data='{"location": "storage/two"}')
|
|
||||||
two = storage.get_storage('two')
|
|
||||||
self.assertEqual(two.base_location, 'storage/two')
|
|
||||||
|
|
||||||
# modify the settings
|
|
||||||
o = UserStorage.objects.get(name='one')
|
|
||||||
o.settings_data = '{"location": "other/one"}'
|
|
||||||
o.save()
|
|
||||||
|
|
||||||
# signal should have fired and invalidated 'one'
|
|
||||||
one = storage.get_storage('one')
|
|
||||||
self.assertEqual(one.base_location, 'other/one')
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPStorageTestCase(TestCase):
|
|
||||||
|
|
||||||
def test_url(self):
|
|
||||||
s = HTTPStorage()
|
|
||||||
|
|
||||||
self.assertEqual(s.url('//google.com'), 'https://google.com')
|
|
||||||
|
|
||||||
def test_exists(self):
|
|
||||||
s = HTTPStorage()
|
|
||||||
|
|
||||||
self.assertTrue(s.exists('//gitea.tfconsulting.com.au/tris'))
|
|
||||||
self.assertFalse(s.exists('//gitea.tfconsulting.com.au/foo.txt'))
|
|
||||||
|
|
||||||
def test_save(self):
|
|
||||||
s = HTTPStorage()
|
|
||||||
with self.assertRaisesMessage(NotImplementedError, "Unable to save to web locations"):
|
|
||||||
s.save('//gitea.tfconsulting.com.au/foo', 'Some content')
|
|
||||||
|
|
||||||
def test_open(self):
|
|
||||||
s = HTTPStorage()
|
|
||||||
with s.open('//gitea.tfconsulting.com.au', 'rb') as f:
|
|
||||||
data = f.read()
|
|
||||||
|
|
||||||
self.assertTrue(len(data) > 4000)
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
from django.db.models.signals import post_save
|
|
||||||
|
|
||||||
from .models import UserStorage
|
|
||||||
from .multi import MultiStorage
|
|
||||||
|
|
||||||
class BYOStorage(MultiStorage):
|
|
||||||
''' Database driven Bring-Your-Own-Storage
|
|
||||||
|
|
||||||
Multiple storages can
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __init__(self, config=None):
|
|
||||||
super(BYOStorage, self).__init__(config)
|
|
||||||
|
|
||||||
post_save.connect(self.handle_change, sender=UserStorage)
|
|
||||||
|
|
||||||
|
|
||||||
def handle_change(self, instance, **kwargs):
|
|
||||||
try:
|
|
||||||
del(self._cache[instance.name])
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
obj = UserStorage.objects.get(name=name)
|
|
||||||
self._cache[name] = obj.instance()
|
|
||||||
return self._cache[name]
|
|
||||||
Loading…
x
Reference in New Issue
Block a user