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