Verified Commit 008d96c6 authored by Štěpán Henek's avatar Štěpán Henek 🌩

generated static files

* it generates js from templates to /tmp/.foris_workdir/...
* these files are served as ordinary static files ('/static/generated/...')
* the files are generated when a link is rendered (typically in base template)
* each language should have own set of generated files
* removed render_js* which are deprecaded
parent 2f30d229
......@@ -48,6 +48,10 @@ def get_arg_parser():
"--wss-path", default="/foris-ws", help="websocket server url path - secure", type=str,
"-A", "--assets", default="/tmp/.foris_workdir/dynamic_assets",
help="Path where dynamic foris assets will be generated."
return parser
......@@ -60,6 +64,8 @@ def main():
current_state.set_backend(Backend(args.backend, args.backend_socket))
# update websocket
current_state.set_websocket(args.ws_port, args.ws_path, args.wss_port, args.wss_path)
# set assets path
if == "config":
from foris.config_app import prepare_config_app
......@@ -85,44 +85,6 @@ def foris_403_handler(error):
def render_js(filename):
""" Render javascript template to insert a translation
:param filename: name of the file to be translated
headers = {}
# check the template file
path ="javascript/%s" % filename, bottle.TEMPLATE_PATH)
if not path:
return bottle.HTTPError(404, "File does not exist.")
# test last modification date (mostly copied from
stats = os.stat(path)
lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime))
headers['Last-Modified'] = lm
ims = bottle.request.environ.get('HTTP_IF_MODIFIED_SINCE')
if ims:
ims = bottle.parse_date(ims.split(";")[0].strip())
if ims is not None and ims >= int(stats.st_mtime):
headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime())
return bottle.HTTPResponse(status=304, **bottle.response.headers)
# set the content type to javascript
headers['Content-Type'] = "application/javascript; charset=UTF-8"
body = bottle.template("javascript/%s" % filename)
# TODO if you are sadistic enough you can try to minify the content
return bottle.HTTPResponse(body, **headers)
def render_js_md5(filename):
# calculate the hash of the rendered template
return hashlib.md5(bottle.template("javascript/%s" % filename).encode('utf-8')).hexdigest()
def change_lang(lang):
"""Change language of the interface.
......@@ -146,6 +108,10 @@ def static(filename):
:type filename: str
:return: http response
def prepare_response(filename, fs_root):
response = bottle.static_file(filename, root=fs_root)
response.add_header("Cache-Control", "public, max-age=31536000")
return response
if not bottle.DEBUG:
logger.warning("Static files should be handled externally in production mode.")
......@@ -157,12 +123,18 @@ def static(filename):
# find correspoding plugin
for plugin in
if plugin.PLUGIN_NAME == plugin_name:
return bottle.static_file(
plugin_file, root=os.path.join(plugin.DIRNAME, "static"))
return prepare_response(plugin_file, os.path.join(plugin.DIRNAME, "static"))
return bottle.HTTPError(404, "File does not exist.")
match = re.match(r'/*generated/+([a-z]{2})/+(.+)', filename)
if match:
filename = "/".join(match.groups())
return prepare_response(
filename, os.path.join(current_state.assets_path,
response = bottle.static_file(filename, root=os.path.join(BASE_DIR, "static"))
response.add_header("Cache-Control", "public, max-age=31536000")
return response
return prepare_response(filename, os.path.join(BASE_DIR, "static"))
def require_contract_valid(valid=True):
......@@ -244,7 +216,6 @@ def init_default_app(index, include_static=False):
app.route("/leave_guide", method="POST", name="leave_guide", callback=leave_guide)
if include_static:
app.route('/static/<filename:re:.*>', name="static", callback=static)
app.route("/js/<filename:re:.*>", name="render_js", callback=render_js)
# route for testing whether the foris app is alive (used in js)
app.route("/ping", name="ping", method=("GET", "OPTIONS"), callback=ping)
return app
......@@ -35,6 +35,7 @@ from foris.utils.bottle_stuff import (
from foris.utils import messages
from foris.utils import dynamic_assets
def prepare_common_app(args, app_name, init_function, top_index, logger, load_plugins=True):
......@@ -117,4 +118,7 @@ def prepare_common_app(args, app_name, init_function, top_index, logger, load_pl
routes = route_list_debug(
logger.debug("Routes:\n%s", "\n".join(routes))
# Make dynamic assets cleanup
dynamic_assets.reset(app_name, args.assets)
return app
......@@ -23,7 +23,6 @@ import bottle
# local
from foris.config import init_app as init_app_config, top_index
from foris.common import render_js_md5
from foris.common_app import prepare_common_app
from foris.utils import contract_valid
......@@ -32,7 +31,6 @@ logger = logging.getLogger("foris.config")
bottle.SimpleTemplate.defaults['contract_valid'] = contract_valid
bottle.SimpleTemplate.defaults['js_md5'] = lambda filename: render_js_md5(filename)
def prepare_config_app(args):
......@@ -30,6 +30,7 @@ class ForisState(object):
self.language = DEFAULT_LANGUAGE = None
self.reboot_required = False
self.assets_path = None
def update_lang(self, lang):
logger.debug("current lang updated to '%s'" % lang)
......@@ -85,5 +86,9 @@ class ForisState(object):
logger.debug("setting guide_data (%s)" % guide_data) = Guide(guide_data)
def set_assets_path(self, assets_path):
logger.debug("setting assets_path to '%s'" % assets_path)
self.assets_path = assets_path
current_state = ForisState()
......@@ -50,8 +50,8 @@
<script src="{{ static("js/contrib/parsley.min.js") }}"></script>
<script src="{{ static("js/parsley.foris-extend.js") }}"></script>
<script src="{{ static("js/foris.js") }}"></script>
<script src="{{ url("render_js", filename="foris.js") }}?md5={{ js_md5('foris.js') }}"></script>
<script src="{{ url("render_js", filename="parsley.messages.js") }}?md5={{ js_md5('parsley.messages.js') }}"></script>
<script src="{{ generated_static("javascript/foris.js") }}"></script>
<script src="{{ generated_static("javascript/parsley.messages.js") }}"></script>
%for static_filename in PLUGIN_STATIC_SCRIPTS:
<script src="{{ static("plugins/%s/%s" % (PLUGIN_NAME, static_filename)) }}"></script>
......@@ -59,7 +59,7 @@
%for filename in PLUGIN_DYNAMIC_SCRIPTS:
<script src="{{ url("render_js", filename=PLUGIN_NAME + "/" + filename) }}?md5={{ js_md5(PLUGIN_NAME + "/" + filename) }}"></script>
<script src="{{ generated_static("javascript/" + PLUGIN_NAME + "/" + filename) }}"></script>
......@@ -22,7 +22,7 @@ from foris.langs import iso2to3, translation_names
from foris.middleware.bottle_csrf import get_csrf_token
from foris.state import current_state
from .routing import reverse, static as static_path
from .routing import reverse, static as static_path, generated_static
from .translators import translations, ugettext, ungettext
from . import is_user_authenticated, template_helpers
......@@ -44,6 +44,7 @@ def prepare_template_defaults():
bottle.SimpleTemplate.defaults["request"] = bottle.request
bottle.SimpleTemplate.defaults["url"] = lambda name, **kwargs: reverse(name, **kwargs)
bottle.SimpleTemplate.defaults["static"] = static_path
bottle.SimpleTemplate.defaults["generated_static"] = generated_static
bottle.SimpleTemplate.defaults["get_csrf_token"] = get_csrf_token
bottle.SimpleTemplate.defaults["helpers"] = template_helpers
# Foris - web administration interface for OpenWrt based on NETCONF
# Copyright (C) 2018 CZ.NIC, z.s.p.o. <>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <>.
import logging
import shutil
import os
import hashlib
import bottle
logger = logging.getLogger("foris.utils.dynamic_assets")
dynamic_assets_map = {
current_assets_path = None
def reset(app_name, assets_path):
""" Deletes generated assets and cleans md5
global dynamic_assets_map
dynamic_assets_map = {}
target = os.path.join(assets_path, app_name)
logger.debug("cleaning dynamic assets in '%s'", target)
shutil.rmtree(target, ignore_errors=True)
global current_assets_path
current_assets_path = target
def store_template(template_name, lang):
""" Creates static file from template as stores it
:param template_name: should looks like this <path>/<file>.tpl
:type template_name: str
template_name = template_name.lstrip("/")
logger.debug("Trying to store generated template '%s' (%s)", template_name, lang)
# already present
if (template_name, lang) in dynamic_assets_map:
logger.debug("Template already generated '%s' (%s)", template_name, lang)
# store file
rendered = bottle.template(template_name + ".tpl")
target_path = os.path.join(current_assets_path, lang, template_name)
except os.error:
# already exists
with open(target_path, "w") as f:
f.write(bytearray(rendered, "utf8"))
"Generated template '%s' (%s) was stored to '%s'.", template_name, lang, target_path)
# mark present
dynamic_assets_map[(template_name, lang)] = True
def regenerate(app_name, target_path, static_files_path, plugin_name=None):
:param plugin_name: None means no plugin just main app
target = os.path.join(target_path, app_name)
shutil.rmtree(target, ignore_errors=True)
# handle static files
for root, _, files in os.walk(static_files_path):
target_dir = root.replace(static_files_path, "", 1)
target_dir = target_dir.lstrip("/")
target_dir = os.path.join("static", target_dir)
# TODO not that make dir has attribute exist_ok in pyton3 so it can be refactored
# after migrating to python3
os.makedirs(os.path.join(target, target_dir))
except os.error:
pass # already created
for filename in files:
# copy file and store md5
asset_name = os.path.join(target_dir, filename)
src_path = os.path.join(root, filename)
dest_path = os.path.join(target, target_dir, filename)
md5 = hashlib.md5()
with open(src_path) as src, open(dest_path, "w") as dest:
content =
digest = md5.hexdigest()
logger.debug("static asset loaded %s '%s'" % (digest, asset_name))
asset_map[asset_name] = {"md5": digest}
......@@ -21,6 +21,8 @@ import os
import re
from foris import BASE_DIR
from foris.state import current_state
from foris.utils.dynamic_assets import store_template
logger = logging.getLogger("utils.routing")
static_md5_map = {
......@@ -72,6 +74,13 @@ def reverse(name, **kargs):
raise bottle.RouteBuildError("No route with name '%s' in main app or mounted apps." % name)
def generated_static(name, *args):
lang =
store_template(name, lang)
name = "generated/%s/%s" % (lang, name.lstrip("/"))
return static(name, *args)
def static(name, *args):
script_name, _ = _get_prefix_and_script_name()
script_name = script_name.strip('/')
......@@ -100,13 +109,19 @@ def static_md5(filename):
if plugin.PLUGIN_NAME == plugin_name:
os_path = os.path.join(plugin.DIRNAME, "static", plugin_file)
os_path = os.path.join(BASE_DIR, filename)
match = re.match(r'(?:static)?/*generated/+([a-z]{2})/+(.+)', filename)
if match:
language, template = match.groups()
os_path = os.path.join(current_state.assets_path,, language, template)
os_path = os.path.join(BASE_DIR, filename)
if not os_path:
logger.warning("Unable to find file for static url '%s'" % filename)
return None
if not os.path.exists(os_path):
raise Exception(os_path)
logger.warning("Static file '%s' related to url '%s' does not exist" % (os_path, filename))
return None
md5 = hashlib.md5()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment