Small changes to fixing transcode percentage
[mediagoblin.git] / mediagoblin / app.py
CommitLineData
8e1e744d 1# GNU MediaGoblin -- federated, autonomous media hosting
cf29e8a8 2# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
e5572c60
ML
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
571198c9 17import os
ec97c937 18import logging
a4768df0 19from contextlib import contextmanager
31a8ff42 20
3d914332
E
21from mediagoblin.routing import get_url_map
22from mediagoblin.tools.routing import endpoint_to_controller
7742dcc1 23
f1d06e1d 24from werkzeug.wrappers import Request
e5e2c5e7 25from werkzeug.exceptions import HTTPException
fd61aac7 26from werkzeug.routing import RequestRedirect
19baab1b 27from werkzeug.wsgi import SharedDataMiddleware
31a8ff42 28
7742dcc1 29from mediagoblin import meddleware, __version__
26583b2c 30from mediagoblin.db.util import check_db_up_to_date
c7424612 31from mediagoblin.tools import common, session, translate, template
785b287f 32from mediagoblin.tools.response import render_http_exception
828fc630 33from mediagoblin.tools.theme import register_themes
152a3bfa 34from mediagoblin.tools import request as mg_request
c81186dd 35from mediagoblin.media_types.tools import media_type_warning
6e7ce8d1 36from mediagoblin.mg_globals import setup_globals
073b61fe 37from mediagoblin.init.celery import setup_celery_from_config
29b6f917 38from mediagoblin.init.plugins import setup_plugins
50854db0 39from mediagoblin.init import (get_jinja_loader, get_staticdirector,
6ef75af5 40 setup_global_and_app_config, setup_locales, setup_workbench, setup_database,
9e1fa239 41 setup_storage)
c5d8d301 42from mediagoblin.tools.pluginapi import PluginManager, hook_transform
5907154a 43from mediagoblin.tools.crypto import setup_crypto
c9dec8b3 44from mediagoblin.auth.tools import check_auth_enabled, no_auth_logout
90e342f9 45
b88ca698
CAW
46from mediagoblin.tools.transition import DISABLE_GLOBALS
47
31a8ff42 48
ec97c937
E
49_log = logging.getLogger(__name__)
50
51
d7149900
CAW
52class 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
8e1e744d 65class MediaGoblinApp(object):
31a8ff42 66 """
3f5cf663
CAW
67 WSGI application of MediaGoblin
68
69 ... this is the heart of the program!
31a8ff42 70 """
3f5cf663
CAW
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 """
ec97c937 81 _log.info("GNU MediaGoblin %s main server starting", __version__)
3f369674 82 _log.debug("Using config file %s", config_path)
3f5cf663
CAW
83 ##############
84 # Setup config
85 ##############
86
87 # Open and setup the config
b8e2ab2f 88 self.global_config, self.app_config = setup_global_and_app_config(config_path)
3f5cf663 89
c81186dd
RE
90 media_type_warning()
91
b8e2ab2f 92 setup_crypto(self.app_config)
5907154a 93
3f5cf663
CAW
94 ##########################################
95 # Setup other connections / useful objects
96 ##########################################
97
b0ee3aae
E
98 # Setup Session Manager, not needed in celery
99 self.session_manager = session.SessionManager()
100
6ef75af5
SS
101 # load all available locales
102 setup_locales()
103
29b6f917
WKG
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
3f5cf663 109 # Set up the database
c060353e 110 if DISABLE_GLOBALS:
7c563e91 111 self.db_manager = setup_database(self)
c060353e 112 else:
7c563e91 113 self.db = setup_database(self)
ff94114c 114
26583b2c 115 # Quit app if need to run dbupdate
31f8909f
CAW
116 ## NOTE: This is currently commented out due to session errors..
117 ## We'd like to re-enable!
118 # check_db_up_to_date()
26583b2c 119
828fc630 120 # Register themes
b8e2ab2f 121 self.theme_registry, self.current_theme = register_themes(self.app_config)
828fc630 122
5afdd7a1 123 # Get the template environment
42ef819c 124 self.template_loader = get_jinja_loader(
b8e2ab2f 125 self.app_config.get('local_templates'),
8545dd50 126 self.current_theme,
05e007c1 127 PluginManager().get_template_paths()
8545dd50 128 )
0c8a30e6 129
744f1c83
RE
130 # Check if authentication plugin is enabled and respond accordingly.
131 self.auth = check_auth_enabled()
1bce0c15 132 if not self.auth:
b8e2ab2f 133 self.app_config['allow_comments'] = False
744f1c83 134
5afdd7a1 135 # Set up storage systems
dccef262 136 self.public_store, self.queue_store = setup_storage()
5afdd7a1
CAW
137
138 # set up routing
48cf435d 139 self.url_map = get_url_map()
31a8ff42 140
582c4d5f 141 # set up staticdirector tool
b8e2ab2f 142 self.staticdirector = get_staticdirector(self.app_config)
3f5cf663
CAW
143
144 # Setup celery, if appropriate
b8e2ab2f 145 if setup_celery and not self.app_config.get('celery_setup_elsewhere'):
d9a31a39 146 if os.environ.get('CELERY_ALWAYS_EAGER', 'false').lower() == 'true':
3f5cf663 147 setup_celery_from_config(
b8e2ab2f 148 self.app_config, self.global_config,
3f5cf663
CAW
149 force_celery_always_eager=True)
150 else:
b8e2ab2f 151 setup_celery_from_config(self.app_config, self.global_config)
3f5cf663
CAW
152
153 #######################################################
154 # Insert appropriate things into mediagoblin.mg_globals
155 #
df9809c2
CAW
156 # certain properties need to be accessed globally eg from
157 # validators, etc, which might not access to the request
158 # object.
b88ca698
CAW
159 #
160 # Note, we are trying to transition this out;
161 # run with environment variable DISABLE_GLOBALS=true
162 # to work on it
3f5cf663
CAW
163 #######################################################
164
b88ca698
CAW
165 if not DISABLE_GLOBALS:
166 setup_globals(app=self)
1fd97db3
CAW
167
168 # Workbench *currently* only used by celery, so this only
169 # matters in always eager mode :)
b8e2ab2f 170 self.workbench_manager = setup_workbench()
df9809c2 171
ce5ae8da
CAW
172 # instantiate application meddleware
173 self.meddleware = [common.import_component(m)(self)
174 for m in meddleware.ENABLED_MEDDLEWARE]
0c8a30e6 175
a4768df0 176 @contextmanager
b8e2ab2f 177 def gen_context(self, ctx=None, **kwargs):
d7149900
CAW
178 """
179 Attach contextual information to request, or generate a context object
f1d06e1d 180
d7149900
CAW
181 This avoids global variables; various utilities and contextual
182 information (current translation, etc) are attached to this
183 object.
184 """
a4768df0
CAW
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
b8e2ab2f 191 def _gen_context(self, db, ctx, **kwargs):
d7149900
CAW
192 # Set up context
193 # --------------
31a8ff42 194
d7149900 195 # Is a context provided?
b88ca698 196 if ctx is None:
d7149900
CAW
197 ctx = Context()
198
199 # Attach utilities
200 # ----------------
871fc591 201
3d0557bf
CAW
202 # Attach self as request.app
203 # Also attach a few utilities from request.app for convenience?
d7149900 204 ctx.app = self
0c8a30e6 205
a4768df0 206 ctx.db = db
c060353e 207
d7149900
CAW
208 ctx.staticdirect = self.staticdirector
209
b88ca698
CAW
210 # Do special things if this is a request
211 # --------------------------------------
212 if isinstance(ctx, Request):
213 ctx = self._request_only_gen_context(ctx)
214
d7149900
CAW
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)
3d0557bf 224
1ec7ff2a 225 request.locale = translate.get_locale_from_request(request)
d7149900
CAW
226
227 # This should be moved over for certain, but how to deal with
228 # request.locale?
1ec7ff2a 229 request.template_env = template.get_jinja_env(
753cfc3b 230 self, self.template_loader, request.locale)
7742dcc1 231
d7149900
CAW
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
7742dcc1
JW
237 def build_proxy(endpoint, **kw):
238 try:
239 qualified = kw.pop('qualified')
240 except KeyError:
241 qualified = False
242
d7149900 243 return request.map_adapter.build(
7742dcc1
JW
244 endpoint,
245 values=dict(**kw),
246 force_external=qualified)
247
248 request.urlgen = build_proxy
249
d7149900
CAW
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
a4768df0
CAW
275 with self.gen_context(request) as request:
276 return self._finish_call_backend(request, environ, start_response)
d7149900 277
a4768df0 278 def _finish_call_backend(self, request, environ, start_response):
5101c469 279 # Log user out if authentication_disabled
c9dec8b3
RE
280 no_auth_logout(request)
281
f7a5c7c7 282 request.controller_name = None
1ec7ff2a 283 try:
d7149900 284 found_rule, url_values = request.map_adapter.match(return_rule=True)
1ec7ff2a 285 request.matchdict = url_values
fd61aac7
SS
286 except RequestRedirect as response:
287 # Deal with 301 responses eg due to missing final slash
288 return response(environ, start_response)
1ec7ff2a 289 except HTTPException as exc:
785b287f
SS
290 # Stop and render exception
291 return render_http_exception(
292 request, exc,
293 exc.get_description(environ))(environ, start_response)
1ec7ff2a 294
05501c57 295 controller = endpoint_to_controller(found_rule)
98dacfe6 296 # Make a reference to the controller's symbolic name on the request...
38103094 297 # used for lazy context modification
98dacfe6 298 request.controller_name = found_rule.endpoint
91cf6738 299
d7149900 300 ## TODO: get rid of meddleware, turn it into hooks only
91cf6738 301 # pass the request through our meddleware classes
785b287f
SS
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)
91cf6738 311
b1fbf67e
CAW
312 request = hook_transform("modify_request", request)
313
31a8ff42
CAW
314 request.start_response = start_response
315
785b287f
SS
316 # get the Http response from the controller
317 try:
a4768df0 318 response = controller(request)
785b287f
SS
319 except HTTPException as e:
320 response = render_http_exception(
321 request, e, e.get_description(environ))
0c8a30e6 322
785b287f
SS
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:
6a28bc4e 328 response = render_http_exception(
785b287f 329 request, e, e.get_description(environ))
0c8a30e6 330
d7149900
CAW
331 self.session_manager.save_session_to_cookie(
332 request.session,
333 request, response)
c7424612 334
e824570a
E
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
2bc8ff0d 341 try:
e824570a
E
342 return self.call_backend(environ, start_response)
343 finally:
c060353e
CAW
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()
31a8ff42
CAW
348
349
5784c4e9 350def paste_app_factory(global_config, **app_config):
91903aa6
CAW
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.")
19baab1b 360 del app_config['config']
91903aa6
CAW
361
362 mgoblin_app = MediaGoblinApp(mediagoblin_config)
19baab1b
BP
363 mgoblin_app.call_backend = SharedDataMiddleware(mgoblin_app.call_backend,
364 exports=app_config)
c5d8d301 365 mgoblin_app = hook_transform('wrap_wsgi', mgoblin_app)
f3f53028 366
c4d71564 367 return mgoblin_app