1import importlib 2 3 4class DepotManager(object): 5 """Takes care of managing the whole Depot environment for the application. 6 7 DepotManager tracks the created depots, the current default depot, 8 and the WSGI middleware in charge of serving files for local depots. 9 10 While this is used to create the default depot used by the application it can 11 also create additional depots using the :meth:`new` method. 12 13 In case you need to migrate your application to a different storage while 14 keeping compatibility with previously stored file simply change the default depot 15 through :meth:`set_default` all previously stored file will continue to work 16 on the old depot while new files will be uploaded to the new default one. 17 18 """ 19 _default_depot = None 20 _depots = {} 21 _middleware = None 22 _aliases = {} 23 24 @classmethod 25 def set_default(cls, name): 26 """Replaces the current application default depot""" 27 if name not in cls._depots: 28 raise RuntimeError('%s depot has not been configured' % (name,)) 29 cls._default_depot = name 30 31 @classmethod 32 def get_default(cls): 33 """Retrieves the current application default depot""" 34 if cls._default_depot is None: 35 raise RuntimeError('Not depots have been configured!') 36 return cls._default_depot 37 38 @classmethod 39 def set_middleware(cls, mw): 40 if cls._middleware is not None: 41 raise RuntimeError('There is already a WSGI middleware registered') 42 cls._middleware = mw 43 44 @classmethod 45 def get_middleware(cls): 46 if cls._middleware is None: 47 raise RuntimeError('No WSGI middleware currently registered') 48 return cls._middleware 49 50 @classmethod 51 def get(cls, name=None): 52 """Gets the application wide depot instance. 53 54 Might return ``None`` if :meth:`configure` has not been 55 called yet. 56 57 """ 58 if name is None: 59 name = cls._default_depot 60 61 name = cls.resolve_alias(name) # resolve alias 62 return cls._depots.get(name) 63 64 @classmethod 65 def get_file(cls, path): 66 """Retrieves a file by storage name and fileid in the form of a path 67 68 Path is expected to be ``storage_name/fileid``. 69 """ 70 depot_name, file_id = path.split('/', 1) 71 depot = cls.get(depot_name) 72 return depot.get(file_id) 73 74 @classmethod 75 def url_for(cls, path): 76 """Given path of a file uploaded on depot returns the url that serves it 77 78 Path is expected to be ``storage_name/fileid``. 79 """ 80 mw = cls.get_middleware() 81 return mw.url_for(path) 82 83 @classmethod 84 def configure(cls, name, config, prefix='depot.'): 85 """Configures an application depot. 86 87 This configures the application wide depot from a settings dictionary. 88 The settings dictionary is usually loaded from an application configuration 89 file where all the depot options are specified with a given ``prefix``. 90 91 The default ``prefix`` is *depot.*, the minimum required setting 92 is ``depot.backend`` which specified the required backend for files storage. 93 Additional options depend on the choosen backend. 94 95 """ 96 if name in cls._depots: 97 raise RuntimeError('Depot %s has already been configured' % (name,)) 98 99 if cls._default_depot is None: 100 cls._default_depot = name 101 102 cls._depots[name] = cls.from_config(config, prefix) 103 return cls._depots[name] 104 105 @classmethod 106 def alias(cls, alias, name): 107 if name not in cls._depots: 108 raise ValueError('You can only alias an existing storage, %s not found' % (name, )) 109 110 if alias in cls._depots: 111 raise ValueError('Cannot use an existing storage name as an alias, will break existing files.') 112 113 cls._aliases[alias] = name 114 115 @classmethod 116 def resolve_alias(cls, name): 117 while name and name not in cls._depots: 118 name = cls._aliases.get(name) 119 return name 120 121 @classmethod 122 def make_middleware(cls, app, **options): 123 """Creates the application WSGI middleware in charge of serving local files. 124 125 A Depot middleware is required if your application wants to serve files from 126 storages that don't directly provide and HTTP interface like 127 :class:`depot.io.local.LocalFileStorage` and :class:`depot.io.gridfs.GridFSStorage` 128 129 """ 130 from depot.middleware import DepotMiddleware 131 mw = DepotMiddleware(app, **options) 132 cls.set_middleware(mw) 133 return mw 134 135 @classmethod 136 def _new(cls, backend, **options): 137 module, classname = backend.rsplit('.', 1) 138 backend = importlib.import_module(module) 139 class_ = getattr(backend, classname) 140 return class_(**options) 141 142 @classmethod 143 def from_config(cls, config, prefix='depot.'): 144 """Creates a new depot from a settings dictionary. 145 146 Behaves like the :meth:`configure` method but instead of configuring the application 147 depot it creates a new one each time. 148 """ 149 config = config or {} 150 151 # Get preferred storage backend 152 backend = config.get(prefix + 'backend', 'depot.io.local.LocalFileStorage') 153 154 # Get all options 155 prefixlen = len(prefix) 156 options = dict((k[prefixlen:], config[k]) for k in config.keys() if k.startswith(prefix)) 157 158 # Backend is already passed as a positional argument 159 options.pop('backend', None) 160 return cls._new(backend, **options) 161 162 @classmethod 163 def _clear(cls): 164 """This is only for testing pourposes, resets the DepotManager status 165 166 This is to simplify writing test fixtures, resets the DepotManager global 167 status and removes the informations related to the current configured depots 168 and middleware. 169 """ 170 cls._default_depot = None 171 cls._depots = {} 172 cls._middleware = None 173 cls._aliases = {} 174 175get_depot = DepotManager.get 176get_file = DepotManager.get_file 177configure = DepotManager.configure 178set_default = DepotManager.set_default