app.py 14 KB

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