mirror of
https://github.com/jazzband/django-ddp.git
synced 2026-03-16 22:40:24 +00:00
288 lines
10 KiB
Python
288 lines
10 KiB
Python
"""Django DDP Server views."""
|
|
from __future__ import absolute_import, unicode_literals
|
|
|
|
from copy import deepcopy
|
|
import io
|
|
import logging
|
|
import mimetypes
|
|
import os.path
|
|
|
|
from ejson import dumps, loads
|
|
from django.conf import settings
|
|
from django.http import HttpResponse
|
|
from django.views.generic import View
|
|
|
|
import pybars
|
|
|
|
|
|
# from https://www.xormedia.com/recursively-merge-dictionaries-in-python/
|
|
def dict_merge(a, b):
|
|
'''recursively merges dict's. not just simple a['key'] = b['key'], if
|
|
both a and bhave a key who's value is a dict then dict_merge is called
|
|
on both values and the result stored in the returned dictionary.'''
|
|
if not isinstance(b, dict):
|
|
return b
|
|
result = deepcopy(a)
|
|
for k, v in b.iteritems():
|
|
if k in result and isinstance(result[k], dict):
|
|
result[k] = dict_merge(result[k], v)
|
|
else:
|
|
result[k] = deepcopy(v)
|
|
return result
|
|
|
|
|
|
def read(path, default=None, encoding='utf8'):
|
|
"""Read encoded contents from specified path or return default."""
|
|
if not path:
|
|
return default
|
|
try:
|
|
with io.open(path, mode='r', encoding=encoding) as contents:
|
|
return contents.read()
|
|
except IOError:
|
|
if default is not None:
|
|
return default
|
|
raise
|
|
|
|
|
|
def read_json(path):
|
|
"""Read JSON encoded contents from specified path."""
|
|
with open(path, mode='r') as json_file:
|
|
return loads(json_file.read())
|
|
|
|
|
|
class MeteorView(View):
|
|
|
|
"""Django DDP Meteor server view."""
|
|
|
|
logger = logging.getLogger(__name__)
|
|
http_method_names = ['get', 'head']
|
|
|
|
json_path = None
|
|
runtime_config = None
|
|
|
|
manifest = None
|
|
program_json = None
|
|
program_json_path = None
|
|
|
|
star_json = None # top level layout
|
|
|
|
url_map = None
|
|
internal_map = None
|
|
server_load_map = None
|
|
template_path = None # web server HTML template path
|
|
client_map = None # web.browser (client) URL to path map
|
|
html = '<!DOCTYPE html>\n<html><head><title>DDP App</title></head></html>'
|
|
|
|
meteor_settings = None
|
|
meteor_public_envs = None
|
|
root_url_path_prefix = ''
|
|
bundled_js_css_prefix = '/'
|
|
|
|
def __init__(self, **kwargs):
|
|
"""
|
|
Initialisation for Django DDP server view.
|
|
|
|
`Meteor.settings` is sourced from the following (later take precedence):
|
|
1. django.conf.settings.METEOR_SETTINGS
|
|
2. os.environ['METEOR_SETTINGS']
|
|
3. MeteorView.meteor_settings (class attribute) or empty dict
|
|
4. MeteorView.as_view(meteor_settings=...)
|
|
|
|
Additionally, `Meteor.settings.public` is updated with values from
|
|
environemnt variables specified from the following sources:
|
|
1. django.conf.settings.METEOR_PUBLIC_ENVS
|
|
2. os.environ['METEOR_PUBLIC_ENVS']
|
|
3. MeteorView.meteor_public_envs (class attribute) or empty dict
|
|
4. MeteorView.as_view(meteor_public_envs=...)
|
|
"""
|
|
self.runtime_config = {}
|
|
self.meteor_settings = reduce(
|
|
dict_merge,
|
|
[
|
|
getattr(settings, 'METEOR_SETTINGS', {}),
|
|
loads(os.environ.get('METEOR_SETTINGS', '{}')),
|
|
self.meteor_settings or {},
|
|
kwargs.pop('meteor_settings', {}),
|
|
],
|
|
{},
|
|
)
|
|
self.meteor_public_envs = set()
|
|
self.meteor_public_envs.update(
|
|
getattr(settings, 'METEOR_PUBLIC_ENVS', []),
|
|
os.environ.get('METEOR_PUBLIC_ENVS', '').replace(',', ' ').split(),
|
|
self.meteor_public_envs or [],
|
|
kwargs.pop('meteor_public_envs', []),
|
|
)
|
|
public = self.meteor_settings.setdefault('public', {})
|
|
for env_name in self.meteor_public_envs:
|
|
try:
|
|
public[env_name] = os.environ[env_name]
|
|
except KeyError:
|
|
pass # environment variable not set
|
|
# super(...).__init__ assigns kwargs to instance.
|
|
super(MeteorView, self).__init__(**kwargs)
|
|
|
|
# read and process /etc/mime.types
|
|
mimetypes.init()
|
|
|
|
self.url_map = {}
|
|
|
|
# process `star_json`
|
|
self.star_json = read_json(self.json_path)
|
|
star_format = self.star_json['format']
|
|
if star_format != 'site-archive-pre1':
|
|
raise ValueError(
|
|
'Unknown Meteor star format: %r' % star_format,
|
|
)
|
|
programs = {
|
|
program['name']: program
|
|
for program in self.star_json['programs']
|
|
}
|
|
|
|
# process `bundle/programs/server/program.json` from build dir
|
|
server_json_path = os.path.join(
|
|
os.path.dirname(self.json_path),
|
|
os.path.dirname(programs['server']['path']),
|
|
'program.json',
|
|
)
|
|
server_json = read_json(server_json_path)
|
|
server_format = server_json['format']
|
|
if server_format != 'javascript-image-pre1':
|
|
raise ValueError(
|
|
'Unknown Meteor server format: %r' % server_format,
|
|
)
|
|
self.server_load_map = {}
|
|
for item in server_json['load']:
|
|
item['path_full'] = os.path.join(
|
|
os.path.dirname(server_json_path),
|
|
item['path'],
|
|
)
|
|
self.server_load_map[item['path']] = item
|
|
self.url_map[item['path']] = (
|
|
item['path_full'], 'text/javascript'
|
|
)
|
|
try:
|
|
item['source_map_full'] = os.path.join(
|
|
os.path.dirname(server_json_path),
|
|
item['sourceMap'],
|
|
)
|
|
self.url_map[item['sourceMap']] = (
|
|
item['source_map_full'], 'text/plain'
|
|
)
|
|
except KeyError:
|
|
pass
|
|
self.template_path = os.path.join(
|
|
os.path.dirname(server_json_path),
|
|
self.server_load_map[
|
|
'packages/boilerplate-generator.js'
|
|
][
|
|
'assets'
|
|
][
|
|
'boilerplate_web.browser.html'
|
|
],
|
|
)
|
|
|
|
# process `bundle/programs/web.browser/program.json` from build dir
|
|
web_browser_json_path = os.path.join(
|
|
os.path.dirname(self.json_path),
|
|
programs['web.browser']['path'],
|
|
)
|
|
web_browser_json = read_json(web_browser_json_path)
|
|
web_browser_format = web_browser_json['format']
|
|
if web_browser_format != 'web-program-pre1':
|
|
raise ValueError(
|
|
'Unknown Meteor web.browser format: %r' % (
|
|
web_browser_format,
|
|
),
|
|
)
|
|
self.client_map = {}
|
|
self.internal_map = {}
|
|
for item in web_browser_json['manifest']:
|
|
item['path_full'] = os.path.join(
|
|
os.path.dirname(web_browser_json_path),
|
|
item['path'],
|
|
)
|
|
if item['where'] == 'client':
|
|
if '?' in item['url']:
|
|
item['url'] = item['url'].split('?', 1)[0]
|
|
if item['url'].startswith('/'):
|
|
item['url'] = item['url'][1:]
|
|
self.client_map[item['url']] = item
|
|
self.url_map[item['url']] = (
|
|
item['path_full'],
|
|
mimetypes.guess_type(
|
|
item['path_full'],
|
|
)[0] or 'application/octet-stream',
|
|
)
|
|
elif item['where'] == 'internal':
|
|
self.internal_map[item['type']] = item
|
|
|
|
config = {
|
|
'css': [
|
|
{'url': item['path']}
|
|
for item in web_browser_json['manifest']
|
|
if item['type'] == 'css' and item['where'] == 'client'
|
|
],
|
|
'js': [
|
|
{'url': item['path']}
|
|
for item in web_browser_json['manifest']
|
|
if item['type'] == 'js' and item['where'] == 'client'
|
|
],
|
|
'meteorRuntimeConfig': '"%s"' % (
|
|
dumps(self.runtime_config)
|
|
),
|
|
'rootUrlPathPrefix': self.root_url_path_prefix,
|
|
'bundledJsCssPrefix': self.bundled_js_css_prefix,
|
|
'inlineScriptsAllowed': False,
|
|
'inline': None,
|
|
'head': read(
|
|
self.internal_map.get('head', {}).get('path_full', None),
|
|
default=u'',
|
|
),
|
|
'body': read(
|
|
self.internal_map.get('body', {}).get('path_full', None),
|
|
default=u'',
|
|
),
|
|
}
|
|
tmpl_raw = read(self.template_path, encoding='utf8')
|
|
compiler = pybars.Compiler()
|
|
tmpl = compiler.compile(tmpl_raw)
|
|
self.html = '<!DOCTYPE html>\n%s' % tmpl(config)
|
|
|
|
def get(self, request, path):
|
|
"""Return HTML (or other related content) for Meteor."""
|
|
self.logger.debug(
|
|
'[%s:%s] %s %s %s',
|
|
request.META['REMOTE_ADDR'], request.META['REMOTE_PORT'],
|
|
request.method, request.path,
|
|
request.META['SERVER_PROTOCOL'],
|
|
)
|
|
if path == 'meteor_runtime_config.js':
|
|
config = {
|
|
'DDP_DEFAULT_CONNECTION_URL': request.build_absolute_uri('/'),
|
|
'PUBLIC_SETTINGS': self.meteor_settings.get('public', {}),
|
|
'ROOT_URL': request.build_absolute_uri(
|
|
'%s/' % self.runtime_config.get('ROOT_URL_PATH_PREFIX', ''),
|
|
),
|
|
'ROOT_URL_PATH_PREFIX': '',
|
|
}
|
|
# Use HTTPS instead of HTTP if SECURE_SSL_REDIRECT is set
|
|
if config['DDP_DEFAULT_CONNECTION_URL'].startswith('http:') \
|
|
and settings.SECURE_SSL_REDIRECT:
|
|
config['DDP_DEFAULT_CONNECTION_URL'] = 'https:%s' % (
|
|
config['DDP_DEFAULT_CONNECTION_URL'].split(':', 1)[1],
|
|
)
|
|
config.update(self.runtime_config)
|
|
return HttpResponse(
|
|
'__meteor_runtime_config__ = %s;' % dumps(config),
|
|
content_type='text/javascript',
|
|
)
|
|
try:
|
|
file_path, content_type = self.url_map[path]
|
|
with open(file_path, 'r') as content:
|
|
return HttpResponse(
|
|
content.read(),
|
|
content_type=content_type,
|
|
)
|
|
except KeyError:
|
|
return HttpResponse(self.html)
|