Compare commits
11
Commits
be8954b505
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9903bb0088 | ||
|
|
f134df3384 | ||
|
|
92d64a57d2 | ||
|
|
43db0b0983 | ||
|
|
227ce5fccb | ||
|
|
432118ac58 | ||
|
|
49646dfafa | ||
|
|
4d87edd995 | ||
|
|
4284b4a05f | ||
|
|
845762400f | ||
|
|
0ad2812ed6 |
@@ -23,3 +23,8 @@ Usage
|
|||||||
photo = models.FileField(
|
photo = models.FileField(
|
||||||
storage=BYOStorage(),
|
storage=BYOStorage(),
|
||||||
upload_to=generate_filename)
|
upload_to=generate_filename)
|
||||||
|
|
||||||
|
Notes
|
||||||
|
=====
|
||||||
|
|
||||||
|
Boto3 client sometimes doesn't respect the `region_name` so it may be neccesary to set `endpoint_url: https://https://s3-<region>.amazonaws.com`
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
__version__ = 0.1
|
__version__ = '0.2.4'
|
||||||
default_app_config = 'byostorage.apps.BYOStorageConfig'
|
default_app_config = 'byostorage.apps.BYOStorageConfig'
|
||||||
+13
-5
@@ -1,5 +1,6 @@
|
|||||||
from django.core.files.storage import Storage, get_storage_class
|
from django.core.files.storage import Storage, DefaultStorage
|
||||||
from django.utils.deconstruct import deconstructible
|
from django.utils.deconstruct import deconstructible
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
import os.path
|
import os.path
|
||||||
@@ -15,11 +16,14 @@ class CachedStorage(Storage):
|
|||||||
|
|
||||||
def __init__(self, remote=None, cachedir=None, expires=600):
|
def __init__(self, remote=None, cachedir=None, expires=600):
|
||||||
if not remote:
|
if not remote:
|
||||||
remote = getattr(settings, 'CACHED_STORAGE_REMOTE', None)
|
remote = getattr(settings, 'CACHED_STORAGE_REMOTE', DefaultStorage)
|
||||||
if not cachedir:
|
if not cachedir:
|
||||||
cachedir = getattr(settings, 'CACHED_STORAGE_DIR', 'cache')
|
cachedir = getattr(settings, 'CACHED_STORAGE_DIR', 'cache')
|
||||||
|
|
||||||
self.remote = get_storage_class(remote)()
|
if isinstance(remote, Storage):
|
||||||
|
self.remote = remote
|
||||||
|
else:
|
||||||
|
self.remote = import_string(remote)()
|
||||||
self.cachedir = cachedir
|
self.cachedir = cachedir
|
||||||
self.expires = expires
|
self.expires = expires
|
||||||
|
|
||||||
@@ -52,16 +56,20 @@ class CachedStorage(Storage):
|
|||||||
|
|
||||||
def _open(self, name, mode='rb'):
|
def _open(self, name, mode='rb'):
|
||||||
assert 'r' in mode, "Can only open for reading"
|
assert 'r' in mode, "Can only open for reading"
|
||||||
p = self._cached(name)
|
p = self.path(name)
|
||||||
return open(p, mode)
|
return open(p, mode)
|
||||||
|
|
||||||
def path(self, name):
|
def path(self, name):
|
||||||
return self._cached(name)
|
try:
|
||||||
|
return self.remote.path(name)
|
||||||
|
except NotImplementedError:
|
||||||
|
return self._cached(name)
|
||||||
|
|
||||||
def save(self, name, content, max_length=None):
|
def save(self, name, content, max_length=None):
|
||||||
p = self._filepath(name)
|
p = self._filepath(name)
|
||||||
if os.path.exists(p):
|
if os.path.exists(p):
|
||||||
os.unlink(p)
|
os.unlink(p)
|
||||||
|
# TODO: cache content to p
|
||||||
return self.remote.save(name, content, max_length)
|
return self.remote.save(name, content, max_length)
|
||||||
|
|
||||||
def delete(self, name):
|
def delete(self, name):
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.2.7 on 2022-09-09 23:36
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('byostorage', '0004_auto_20210917_0000'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 3.2.7 on 2022-09-10 00:10
|
||||||
|
|
||||||
|
import byostorage.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('byostorage', '0005_alter_userstorage_storage'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userstorage',
|
||||||
|
name='settings_data',
|
||||||
|
field=models.TextField(default='{}', help_text='JSON dict with key/value settings', validators=[byostorage.models.validate_json]),
|
||||||
|
),
|
||||||
|
]
|
||||||
+13
-3
@@ -2,8 +2,9 @@ from django.conf import settings
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.fields.files import FieldFile
|
from django.db.models.fields.files import FieldFile
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.files.storage import Storage, get_storage_class
|
from django.core.files.storage import Storage
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -18,7 +19,15 @@ STORAGE_CLASSES.extend([
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import storages.backends
|
import storages.backends
|
||||||
STORAGE_CLASSES.append('storages.backends.s3boto3.S3Boto3Storage')
|
|
||||||
|
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:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -53,10 +62,11 @@ class UserStorage(models.Model):
|
|||||||
help_text="Storage class for this instance")
|
help_text="Storage class for this instance")
|
||||||
settings_data = models.TextField(
|
settings_data = models.TextField(
|
||||||
validators=[validate_json],
|
validators=[validate_json],
|
||||||
|
default='{}',
|
||||||
help_text="JSON dict with key/value settings")
|
help_text="JSON dict with key/value settings")
|
||||||
|
|
||||||
def instance(self):
|
def instance(self):
|
||||||
return get_storage_class(self.storage)(**self.settings)
|
return import_string(self.storage)(**self.settings)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def settings(self):
|
def settings(self):
|
||||||
|
|||||||
+8
-4
@@ -1,6 +1,7 @@
|
|||||||
from django.core.files.storage import Storage, default_storage, get_storage_class
|
from django.core.files.storage import Storage, default_storage, storages
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.deconstruct import deconstructible
|
from django.utils.deconstruct import deconstructible
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
|
|
||||||
'''
|
'''
|
||||||
TODO: Create a signal to remove instances from cache if modified
|
TODO: Create a signal to remove instances from cache if modified
|
||||||
@@ -28,7 +29,7 @@ class MultiStorage(Storage):
|
|||||||
config = self.config[name]
|
config = self.config[name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return default_storage
|
return default_storage
|
||||||
klass = get_storage_class(config.pop('storage', None))
|
klass = import_string(config.pop('storage', 'django.core.files.storage.FileSystemStorage'))
|
||||||
self._cache[name] = klass(**config)
|
self._cache[name] = klass(**config)
|
||||||
return self._cache[name]
|
return self._cache[name]
|
||||||
|
|
||||||
@@ -45,18 +46,21 @@ class MultiStorage(Storage):
|
|||||||
def _proxy(self, method, name, *args, **kwargs):
|
def _proxy(self, method, name, *args, **kwargs):
|
||||||
storage, p = self.split(name)
|
storage, p = self.split(name)
|
||||||
result = getattr(self.get_storage(storage), method)(p, *args, **kwargs)
|
result = getattr(self.get_storage(storage), method)(p, *args, **kwargs)
|
||||||
logger.debug("Proxy: %s(%r, *%r, **%r) => %r", method, name, args, kwargs, result)
|
logger.debug("Proxy: %s(%r, *%r, **%r) => %r", method, p, args, kwargs, result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _proxy_name(self, method, name, *args, **kwargs):
|
def _proxy_name(self, method, name, *args, **kwargs):
|
||||||
storage, p = self.split(name)
|
storage, p = self.split(name)
|
||||||
result = getattr(self.get_storage(storage), method)(p, *args, **kwargs)
|
result = getattr(self.get_storage(storage), method)(p, *args, **kwargs)
|
||||||
logger.debug("Proxy: %s(%r, *%r, **%r) => %s:%s", method, name, args, kwargs, storage, result)
|
logger.debug("Proxy: %s(%r, *%r, **%r) => %s:%s", method, p, args, kwargs, storage, result)
|
||||||
return self.join(storage, result)
|
return self.join(storage, result)
|
||||||
|
|
||||||
def _open(self, name, mode='rb'):
|
def _open(self, name, mode='rb'):
|
||||||
return self._proxy('open', name, mode)
|
return self._proxy('open', name, mode)
|
||||||
|
|
||||||
|
def path(self, name):
|
||||||
|
return self._proxy('path', name)
|
||||||
|
|
||||||
def save(self, name, content, max_length=None):
|
def save(self, name, content, max_length=None):
|
||||||
return self._proxy_name('save', name, content, max_length)
|
return self._proxy_name('save', name, content, max_length)
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class HTTPStorageTestCase(TestCase):
|
|||||||
self.assertFalse(s.exists('//gitea.tfconsulting.com.au/foo.txt'))
|
self.assertFalse(s.exists('//gitea.tfconsulting.com.au/foo.txt'))
|
||||||
|
|
||||||
def test_save(self):
|
def test_save(self):
|
||||||
|
return # already caught as suspicious activity
|
||||||
s = HTTPStorage()
|
s = HTTPStorage()
|
||||||
with self.assertRaisesMessage(NotImplementedError, "Unable to save to web locations"):
|
with self.assertRaisesMessage(NotImplementedError, "Unable to save to web locations"):
|
||||||
s.save('//gitea.tfconsulting.com.au/foo', 'Some content')
|
s.save('//gitea.tfconsulting.com.au/foo', 'Some content')
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from django.test import TestCase
|
|||||||
from byostorage.multi import MultiStorage
|
from byostorage.multi import MultiStorage
|
||||||
from django.core.files.storage import FileSystemStorage
|
from django.core.files.storage import FileSystemStorage
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
|
||||||
class MultiStorageTestCase(TestCase):
|
class MultiStorageTestCase(TestCase):
|
||||||
|
|
||||||
@@ -34,3 +35,10 @@ class MultiStorageTestCase(TestCase):
|
|||||||
})
|
})
|
||||||
|
|
||||||
self.assertEqual(ms.url('https://google.com'), 'https://google.com')
|
self.assertEqual(ms.url('https://google.com'), 'https://google.com')
|
||||||
|
|
||||||
|
def test_local_methods(self):
|
||||||
|
ms = MultiStorage({
|
||||||
|
'local': {'storage': 'django.core.files.storage.FileSystemStorage', 'location': 'local_store'}
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(ms.path('local:foo/bar.txt'), os.path.realpath('local_store/foo/bar.txt'))
|
||||||
@@ -11,9 +11,8 @@ classifiers =
|
|||||||
Development Status :: 1 - Development
|
Development Status :: 1 - Development
|
||||||
Environment :: Web Environment
|
Environment :: Web Environment
|
||||||
Framework :: Django
|
Framework :: Django
|
||||||
Framework :: Django :: 2.2
|
Framework :: Django :: 4.2
|
||||||
Framework :: Django :: 3.0
|
Framework :: Django :: 5.0
|
||||||
Framework :: Django :: 3.1
|
|
||||||
Intended Audience :: Developers
|
Intended Audience :: Developers
|
||||||
License :: OSI Approved :: BSD License
|
License :: OSI Approved :: BSD License
|
||||||
Operating System :: OS Independent
|
Operating System :: OS Independent
|
||||||
@@ -29,7 +28,7 @@ classifiers =
|
|||||||
[options]
|
[options]
|
||||||
python_requires = >=3.5
|
python_requires = >=3.5
|
||||||
install_requires =
|
install_requires =
|
||||||
Django >= 2.2
|
Django >= 4.2
|
||||||
packages =
|
packages =
|
||||||
byostorage
|
byostorage
|
||||||
byostorage.migrations
|
byostorage.migrations
|
||||||
|
|||||||
Reference in New Issue
Block a user