Handle collection in batchaddmedia command
[mediagoblin.git] / mediagoblin / app.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17 import os
18 import logging
19 from contextlib import contextmanager
20
21 from mediagoblin.routing import get_url_map
22 from mediagoblin.tools.routing import endpoint_to_controller
23
24 from werkzeug.wrappers import Request
25 from werkzeug.exceptions import HTTPException
26 from werkzeug.routing import RequestRedirect
27 from werkzeug.wsgi import SharedDataMiddleware
28
29 from mediagoblin import meddleware, __version__
30 from mediagoblin.db.util import check_db_up_to_date
31 from mediagoblin.tools import common, session, translate, template
32 from mediagoblin.tools.response import render_http_exception
33 from mediagoblin.tools.theme import register_themes
34 from mediagoblin.tools import request as mg_request
35 from mediagoblin.media_types.tools import media_type_warning
36 from mediagoblin.mg_globals import setup_globals
37 from mediagoblin.init.celery import setup_celery_from_config
38 from mediagoblin.init.plugins import setup_plugins
39 from mediagoblin.init import (get_jinja_loader, get_staticdirector,
40 setup_global_and_app_config, setup_locales, setup_workbench, setup_database,
41 setup_storage)
42 from mediagoblin.tools.pluginapi import PluginManager, hook_transform
43 from mediagoblin.tools.crypto import setup_crypto
44 from mediagoblin.auth.tools import check_auth_enabled, no_auth_logout
45
46 from mediagoblin.tools.transition import DISABLE_GLOBALS
47
48
49 _log = logging.getLogger(__name__)
50
51
52 class Context(object):
53 """
54 MediaGoblin context object.
55
56 If a web request is being used, a Flask Request object is used
57 instead, otherwise (celery tasks, etc), attach things to this
58 object.
59
60 Usually appears as "ctx" in utilities as first argument.
61 """
62 pass
63
64
65 class MediaGoblinApp(object):
66 """
67 WSGI application of MediaGoblin
68
69 ... this is the heart of the program!
70 """
71 def __init__(self, config_path, setup_celery=True):
72 """
73 Initialize the application based on a configuration file.
74
75 Arguments:
76 - config_path: path to the configuration file we're opening.
77 - setup_celery: whether or not to setup celery during init.
78 (Note: setting 'celery_setup_elsewhere' also disables
79 setting up celery.)
80 """
81 _log.info("GNU MediaGoblin %s main server starting", __version__)
82 _log.debug("Using config file %s", config_path)
83 ##############
84 # Setup config
85 ##############
86
87 # Open and setup the config
88 self.global_config, self.app_config = setup_global_and_app_config(config_path)
89
90 media_type_warning()
91
92 setup_crypto(self.app_config)
93
94 ##########################################
95 # Setup other connections / useful objects
96 ##########################################
97
98 # Setup Session Manager, not needed in celery
99 self.session_manager = session.SessionManager()
100
101 # load all available locales
102 setup_locales()
103
104 # Set up plugins -- need to do this early so that plugins can
105 # affect startup.
106 _log.info("Setting up plugins.")
107 setup_plugins()
108
109 # Set up the database
110 if DISABLE_GLOBALS:
111 self.db_manager = setup_database(self)
112 else:
113 self.db = setup_database(self)
114
115 # Quit app if need to run dbupdate
116 ## NOTE: This is currently commented out due to session errors..
117 ## We'd like to re-enable!
118 # check_db_up_to_date()
119
120 # Register themes
121 self.theme_registry, self.current_theme = register_themes(self.app_config)
122
123 # Get the template environment
124 self.template_loader = get_jinja_loader(
125 self.app_config.get('local_templates'),
126 self.current_theme,
127 PluginManager().get_template_paths()
128 )
129
130 # Check if authentication plugin is enabled and respond accordingly.
131 self.auth = check_auth_enabled()
132 if not self.auth:
133 self.app_config['allow_comments'] = False
134
135 # Set up storage systems
136 self.public_store, self.queue_store = setup_storage()
137
138 # set up routing
139 self.url_map = get_url_map()
140
141 # set up staticdirector tool
142 self.staticdirector = get_staticdirector(self.app_config)
143
144 # Setup celery, if appropriate
145 if setup_celery and not self.app_config.get('celery_setup_elsewhere'):
146 if os.environ.get('CELERY_ALWAYS_EAGER', 'false').lower() == 'true':
147 setup_celery_from_config(
148 self.app_config, self.global_config,
149 force_celery_always_eager=True)
150 else:
151 setup_celery_from_config(self.app_config, self.global_config)
152
153 #######################################################
154 # Insert appropriate things into mediagoblin.mg_globals
155 #
156 # certain properties need to be accessed globally eg from
157 # validators, etc, which might not access to the request
158 # object.
159 #
160 # Note, we are trying to transition this out;
161 # run with environment variable DISABLE_GLOBALS=true
162 # to work on it
163 #######################################################
164
165 if not DISABLE_GLOBALS:
166 setup_globals(app=self)
167
168 # Workbench *currently* only used by celery, so this only
169 # matters in always eager mode :)
170 self.workbench_manager = setup_workbench()
171
172 # instantiate application meddleware
173 self.meddleware = [common.import_component(m)(self)
174 for m in meddleware.ENABLED_MEDDLEWARE]
175
176 @contextmanager
177 def gen_context(self, ctx=None, **kwargs):
178 """
179 Attach contextual information to request, or generate a context object
180
181 This avoids global variables; various utilities and contextual
182 information (current translation, etc) are attached to this
183 object.
184 """
185 if DISABLE_GLOBALS:
186 with self.db_manager.session_scope() as db:
187 yield self._gen_context(db, ctx)
188 else:
189 yield self._gen_context(self.db, ctx)
190
191 def _gen_context(self, db, ctx, **kwargs):
192 # Set up context
193 # --------------
194
195 # Is a context provided?
196 if ctx is None:
197 ctx = Context()
198
199 # Attach utilities
200 # ----------------
201
202 # Attach self as request.app
203 # Also attach a few utilities from request.app for convenience?
204 ctx.app = self
205
206 ctx.db = db
207
208 ctx.staticdirect = self.staticdirector
209
210 # Do special things if this is a request
211 # --------------------------------------
212 if isinstance(ctx, Request):
213 ctx = self._request_only_gen_context(ctx)
214
215 return ctx
216
217 def _request_only_gen_context(self, request):
218 """
219 Requests get some extra stuff attached to them that's not relevant
220 otherwise.
221 """
222 # Do we really want to load this via middleware? Maybe?
223 request.session = self.session_manager.load_session_from_cookie(request)
224
225 request.locale = translate.get_locale_from_request(request)
226
227 # This should be moved over for certain, but how to deal with
228 # request.locale?
229 request.template_env = template.get_jinja_env(
230 self, self.template_loader, request.locale)
231
232 mg_request.setup_user_in_request(request)
233
234 ## Routing / controller loading stuff
235 request.map_adapter = self.url_map.bind_to_environ(request.environ)
236
237 def build_proxy(endpoint, **kw):
238 try:
239 qualified = kw.pop('qualified')
240 except KeyError:
241 qualified = False
242
243 return request.map_adapter.build(
244 endpoint,
245 values=dict(**kw),
246 force_external=qualified)
247
248 request.urlgen = build_proxy
249
250 return request
251
252 def call_backend(self, environ, start_response):
253 request = Request(environ)
254
255 # Compatibility with django, use request.args preferrably
256 request.GET = request.args
257
258 # By using fcgi, mediagoblin can run under a base path
259 # like /mediagoblin/. request.path_info contains the
260 # path inside mediagoblin. If the something needs the
261 # full path of the current page, that should include
262 # the basepath.
263 # Note: urlgen and routes are fine!
264 request.full_path = environ["SCRIPT_NAME"] + request.path
265 # python-routes uses SCRIPT_NAME. So let's use that too.
266 # The other option would be:
267 # request.full_path = environ["SCRIPT_URL"]
268
269 # Fix up environ for urlgen
270 # See bug: https://bitbucket.org/bbangert/routes/issue/55/cache_hostinfo-breaks-on-https-off
271 if environ.get('HTTPS', '').lower() == 'off':
272 environ.pop('HTTPS')
273
274 ## Attach utilities to the request object
275 with self.gen_context(request) as request:
276 return self._finish_call_backend(request, environ, start_response)
277
278 def _finish_call_backend(self, request, environ, start_response):
279 # Log user out if authentication_disabled
280 no_auth_logout(request)
281
282 request.controller_name = None
283 try:
284 found_rule, url_values = request.map_adapter.match(return_rule=True)
285 request.matchdict = url_values
286 except RequestRedirect as response:
287 # Deal with 301 responses eg due to missing final slash
288 return response(environ, start_response)
289 except HTTPException as exc:
290 # Stop and render exception
291 return render_http_exception(
292 request, exc,
293 exc.get_description(environ))(environ, start_response)
294
295 controller = endpoint_to_controller(found_rule)
296 # Make a reference to the controller's symbolic name on the request...
297 # used for lazy context modification
298 request.controller_name = found_rule.endpoint
299
300 ## TODO: get rid of meddleware, turn it into hooks only
301 # pass the request through our meddleware classes
302 try:
303 for m in self.meddleware:
304 response = m.process_request(request, controller)
305 if response is not None:
306 return response(environ, start_response)
307 except HTTPException as e:
308 return render_http_exception(
309 request, e,
310 e.get_description(environ))(environ, start_response)
311
312 request = hook_transform("modify_request", request)
313
314 request.start_response = start_response
315
316 # get the Http response from the controller
317 try:
318 response = controller(request)
319 except HTTPException as e:
320 response = render_http_exception(
321 request, e, e.get_description(environ))
322
323 # pass the response through the meddlewares
324 try:
325 for m in self.meddleware[::-1]:
326 m.process_response(request, response)
327 except HTTPException as e:
328 response = render_http_exception(
329 request, e, e.get_description(environ))
330
331 self.session_manager.save_session_to_cookie(
332 request.session,
333 request, response)
334
335 return response(environ, start_response)
336
337 def __call__(self, environ, start_response):
338 ## If more errors happen that look like unclean sessions:
339 # self.db.check_session_clean()
340
341 try:
342 return self.call_backend(environ, start_response)
343 finally:
344 if not DISABLE_GLOBALS:
345 # Reset the sql session, so that the next request
346 # gets a fresh session
347 self.db.reset_after_request()
348
349
350 def paste_app_factory(global_config, **app_config):
351 configs = app_config['config'].split()
352 mediagoblin_config = None
353 for config in configs:
354 if os.path.exists(config) and os.access(config, os.R_OK):
355 mediagoblin_config = config
356 break
357
358 if not mediagoblin_config:
359 raise IOError("Usable mediagoblin config not found.")
360 del app_config['config']
361
362 mgoblin_app = MediaGoblinApp(mediagoblin_config)
363 mgoblin_app.call_backend = SharedDataMiddleware(mgoblin_app.call_backend,
364 exports=app_config)
365 mgoblin_app = hook_transform('wrap_wsgi', mgoblin_app)
366
367 return mgoblin_app