Compare commits

..
11 Commits
Author SHA1 Message Date
Tris Forster 9903bb0088 Updated cache to not use get_storage_class 2024-08-26 11:55:38 +10:00
Tris Forster f134df3384 Version bump 2024-08-26 11:49:10 +10:00
Tris Forster 92d64a57d2 get_storage_class removed in Django 5 2024-08-26 11:37:29 +10:00
tris 43db0b0983 Update 'README.rst' 2023-02-19 21:37:40 +11:00
Tris Forster 227ce5fccb Fixed passing Storage instance 2022-12-21 08:22:14 +11:00
Tris Forster 432118ac58 Version bump 2022-12-21 08:08:36 +11:00
Tris Forster 49646dfafa Dont cache if local 2022-12-21 08:03:40 +11:00
Tris 4d87edd995 Better defaults for user storage 2022-10-19 09:17:14 +11:00
Tris 4284b4a05f Added missing path() method 2022-10-19 09:02:34 +11:00
Tris 845762400f Add all storages classes 2022-09-10 15:25:13 +10:00
Tris 0ad2812ed6 Version bump 2022-09-10 14:49:14 +10:00
10 changed files with 92 additions and 20 deletions
+5
View File
@@ -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`
+2 -2
View File
@@ -1,2 +1,2 @@
__version__ = 0.1 __version__ = '0.2.4'
default_app_config = 'byostorage.apps.BYOStorageConfig' default_app_config = 'byostorage.apps.BYOStorageConfig'
+14 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+2 -1
View File
@@ -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')
@@ -25,4 +26,4 @@ class HTTPStorageTestCase(TestCase):
with s.open('//gitea.tfconsulting.com.au', 'rb') as f: with s.open('//gitea.tfconsulting.com.au', 'rb') as f:
data = f.read() data = f.read()
self.assertTrue(len(data) > 4000) self.assertTrue(len(data) > 4000)
+8
View File
@@ -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'))
+3 -4
View File
@@ -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