from django.core.files.storage import Storage from django.conf import settings from django.utils.deconstruct import deconstructible ''' TODO: Create a signal to remove instances from cache if modified ''' import logging logger = logging.getLogger(__name__) @deconstructible class MultiStorage(Storage): ''' Django storage class that proxies multiple storage classes. Uses a name prefix to determine store e.g. 'images:foo/bar.jpg' ''' DELIM = ":" 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): try: storage, p = name.split(self.DELIM, 1) return storage, p except ValueError: raise ValueError(f"Bad storage path [{name}]") def join(self, storage, name): return self.DELIM.join((storage, name)) def _proxy(self, method, name, *args, **kwargs): storage, p = self.split(name) result = getattr(self.get_storage(storage), method)(p, *args, **kwargs) logger.debug("Proxy: %s(%r, *%r, **%r) => %r", method, name, args, kwargs, result) return result def _proxy_name(self, method, name, *args, **kwargs): storage, p = self.split(name) 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) return self.join(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_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) 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]