diff --git a/etc/nginx.site.example b/etc/nginx.site.example new file mode 100644 index 0000000000000000000000000000000000000000..1c3191ff67f46cd357c7a5f629745debfbccbb4f --- /dev/null +++ b/etc/nginx.site.example @@ -0,0 +1,37 @@ +server { + listen 195.113.20.177:443 ssl; + listen [2001:718:1e03:801::b1]:443 ssl; + + server_name mo.mff.cuni.cz; + + ssl on; + ssl_certificate /etc/ssl/domains/mo.mff.cuni.cz/bundle.pem; + ssl_certificate_key /etc/ssl/domains/mo.mff.cuni.cz/privkey.pem; + ssl_dhparam /etc/ssl/dhparams.pem; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 5m; + + client_max_body_size 1G; + + ### Production instance of OSMO + + location /osmo { + include uwsgi_params; + uwsgi_pass unix:/akce/mo/osmo/var/osmo.sock; + } + + location /osmo/static/ { + alias /akce/mo/osmo/static/; + } + + location /osmo/assets/ { + location ~ ^/osmo/assets/[^/]+/(.*) { + alias /akce/mo/osmo/static/$1; + } + return 404; + } + + location = /osmo/favicon.ico { + alias /akce/mo/osmo/static/favicon.ico; + } +} diff --git a/etc/uwsgi.ini.example b/etc/uwsgi.ini.example index 9b9388b0648206dcde8bb76c9d6b75f017e9a988..86a984570316ff984b6e631ceedacbefe2d3df64 100644 --- a/etc/uwsgi.ini.example +++ b/etc/uwsgi.ini.example @@ -23,4 +23,5 @@ virtualenv = venv manage-script-name = true mule -static-map = /static=static +# Můžeme také pomocí uwsgi servírovat static. Lepší je to dělat Nginxem. +# static-map = /static=static diff --git a/mo/ext/__init__.py b/mo/ext/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..76dd78e636ddca047cdfcf7569b7edee6ffc8f0f --- /dev/null +++ b/mo/ext/__init__.py @@ -0,0 +1,3 @@ +# mo.ext does nothing by itself + +pass diff --git a/mo/ext/assets.py b/mo/ext/assets.py new file mode 100644 index 0000000000000000000000000000000000000000..97a9bcdd5e9c1b189125b8035a9c46efd75b704f --- /dev/null +++ b/mo/ext/assets.py @@ -0,0 +1,62 @@ +# Flask extension for versioned assets + +from flask import send_from_directory +import hashlib +import os +from typing import Sequence, Dict + + +class Assets: + + asset_dir: str + asset_dict: Dict[str, str] + url_prefix: str + + def __init__(self, app, url_prefix: str, asset_dir: str): + self.app = app + if app is not None: + self.init_app(app, url_prefix, asset_dir) + + def init_app(self, app, url_prefix: str, asset_dir: str): + self.asset_folder = asset_dir + self.asset_dict = {} + self.url_prefix = url_prefix + self.app = app + app.jinja_env.globals.update(asset_url=lambda name: self.asset_url(name)) + app.assets = self + + # This is usually needed only for development, production requests are handled by upstream proxy + app.add_url_rule( + url_prefix + "/<version>/<path:name>", + endpoint="assets", + view_func=lambda version, name: self.send_asset(name), + ) + + def add_asset(self, name: str): + if name in self.asset_dict: + return + + file_name = os.path.join(self.asset_folder, name) + digest = hashlib.sha1() + + with open(file_name, 'rb') as file: + while True: + block = file.read(4096) + if not block: + break + digest.update(block) + + version = digest.hexdigest()[:8] + self.app.logger.debug(f'Assets: Loaded {name}: version {version}') + self.asset_dict[name] = version + + def add_assets(self, names: Sequence[str]): + for name in names: + self.add_asset(name) + + def asset_url(self, name: str) -> str: + assert name in self.asset_dict + return os.path.join(self.url_prefix, self.asset_dict[name], name) + + def send_asset(self, name: str): + return send_from_directory(self.asset_folder, name) diff --git a/mo/web/__init__.py b/mo/web/__init__.py index 13b174b6cc500574f399c2a37ddc45bda77feaf5..54024092f1288d253d4fc6b904def893879c835f 100644 --- a/mo/web/__init__.py +++ b/mo/web/__init__.py @@ -14,6 +14,7 @@ import werkzeug.formparser import mo import mo.config as config import mo.db as db +import mo.ext.assets import mo.jobs import mo.rights import mo.users @@ -107,6 +108,15 @@ def setup_logging(): setup_logging() +# Incializace assetového mechanismu +mo.ext.assets.Assets(app, url_prefix='/assets', asset_dir=static_dir) + +app.assets.add_assets([ + 'bootstrap.min.css', + 'mo.css', +]) + + # Inicializace požadavků a nucená autorizace class NeedLoginError(werkzeug.exceptions.Forbidden): @@ -120,7 +130,7 @@ def need_login(): def init_request(): path = request.path - if path.startswith('/static/'): + if path.startswith('/static/') or path.startswith('/assets/'): # Pro statické soubory v development nasazení nepotřebujeme nastavovat # nic dalšího (v ostrém nasazení je servíruje uwsgi) return diff --git a/mo/web/jinja.py b/mo/web/jinja.py index 6b10303d4599e72bbcd3f278885a702f8b68cf23..860146355db7d45a503fd46826d947eab32a9366 100644 --- a/mo/web/jinja.py +++ b/mo/web/jinja.py @@ -1,4 +1,4 @@ -# Konfigurace Jinjový šablon a pomocné funkce +# Konfigurace Jinjových šablon a pomocné funkce from flask import url_for from markupsafe import Markup @@ -45,6 +45,7 @@ app.jinja_env.globals.update(Markup=Markup) app.jinja_env.globals.update(contest_breadcrumbs=contest_breadcrumbs) app.jinja_env.globals.update(place_breadcrumbs=place_breadcrumbs) +# Funkce asset_url se přidává v mo.ext.assets @app.template_filter() diff --git a/mo/web/templates/base.html b/mo/web/templates/base.html index b87201488a68818bf73c55ee3e08391c563453b5..4413fc3dcce773096cbf69aaab6df90900822b1d 100644 --- a/mo/web/templates/base.html +++ b/mo/web/templates/base.html @@ -2,8 +2,8 @@ <html> <head> <title>Odevzdávací systém MO: {% block title %}{% endblock %}</title> - <link rel=stylesheet href="{{ url_for('static', filename='bootstrap.min.css') }}?v=2" type='text/css' media=all> - <link rel=stylesheet href="{{ url_for('static', filename='mo.css') }}?v=7" type='text/css' media=all> + <link rel=stylesheet href="{{ asset_url('bootstrap.min.css') }}" type='text/css' media=all> + <link rel=stylesheet href="{{ asset_url('mo.css') }}" type='text/css' media=all> {% block head %}{% endblock %} </head> <body> diff --git a/setup.py b/setup.py index 073ae8a17a5c03ae695caa6a9b5ee3d65e62a1b8..5b7705e6c8a4f95cd17c41da0ca88e673970ecb9 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setuptools.setup( name='osmo', version='0.1', description='Odevzdávací systém Matematické olympiády', - packages=['mo', 'mo/jobs', 'mo/web'], + packages=['mo', 'mo/ext', 'mo/jobs', 'mo/web'], scripts=[ 'bin/add-role', 'bin/create-contests',