boxespy/scripts/boxesserver

555 lines
20 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright (C) 2016-2017 Florian Festi
#
# 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
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# 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 <http://www.gnu.org/licenses/>.
import sys
import argparse
import html
import tempfile
import os.path
import threading
import time
import codecs
import mimetypes
import re
import markdown
import gettext
import glob
import traceback
from urllib.parse import unquote_plus, quote
from urllib.parse import parse_qs
from wsgiref.util import setup_testing_defaults
from wsgiref.simple_server import make_server
import wsgiref.util
try:
import boxes.generators
except ImportError:
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))
import boxes.generators
class FileChecker(threading.Thread):
def __init__(self, files=[], checkmodules=True):
super(FileChecker, self).__init__()
self.checkmodules = checkmodules
self.timestamps = {}
for path in files:
self.timestamps[path] = os.stat(path).st_mtime
if checkmodules:
self._addModules()
def _addModules(self):
for name, module in sys.modules.items():
path = getattr(module, "__file__", None)
if not path:
continue
if path not in self.timestamps:
self.timestamps[path] = os.stat(path).st_mtime
def filesOK(self):
if self.checkmodules:
self._addModules()
for path, timestamp in self.timestamps.items():
try:
if os.stat(path).st_mtime != timestamp:
return False
except FileNotFoundError:
return False
return True
def run(self):
while True:
if not self.filesOK():
os.execv(__file__, sys.argv)
time.sleep(1)
class ArgumentParserError(Exception): pass
class ThrowingArgumentParser(argparse.ArgumentParser):
def error(self, message):
raise ArgumentParserError(message)
boxes.ArgumentParser = ThrowingArgumentParser # Evil hack
class BServer:
lang_re = re.compile(r"([a-z]{2,3}(-[-a-zA-Z0-9]*)?)\s*(;\s*q=(\d\.?\d*))?")
def __init__(self):
self.boxes = {b.__name__ : b for b in boxes.generators.getAllBoxGenerators().values() if b.webinterface}
self.boxes['TrayLayout2'] = boxes.generators.traylayout.TrayLayout2
self.groups = boxes.generators.ui_groups
self.groups_by_name = boxes.generators.ui_groups_by_name
for name, box in self.boxes.items():
self.groups_by_name.get(box.ui_group,
self.groups_by_name["Misc"]).add(box)
self.staticdir = os.path.join(os.path.dirname(__file__), '../static/')
self._languages = None
def getLanguages(self, domain=None, localedir=None):
if self._languages is not None:
return self._languages
self._languages = []
domain = "boxes.py"
for localedir in ["locale", gettext._default_localedir]:
files = glob.glob(os.path.join(localedir, '*', 'LC_MESSAGES', '%s.mo' % domain))
self._languages.extend([file.split(os.path.sep)[-3] for file in files])
self._languages.sort()
return self._languages
def getLanguage(self, args, accept_language):
lang = None
langs = []
for i, arg in enumerate(args):
if arg.startswith("language="):
lang = arg[len("language="):]
del args[i]
break
if lang:
try:
return gettext.translation('boxes.py', localedir='locale',
languages=[lang])
except OSError:
pass
try:
return gettext.translation('boxes.py', languages=[lang])
except OSError:
pass
# selected language not found try browser default
languages = accept_language.split(",")
for l in languages:
m = self.lang_re.match(l.strip())
if m:
langs.append((float(m.group(4) or 1.0), m.group(1)))
langs.sort(reverse=True)
langs = [l[1].replace("-", "_") for l in langs]
try:
return gettext.translation('boxes.py', localedir='locale',
languages=langs)
except OSError:
return gettext.translation('boxes.py', languages=langs, fallback=True)
def arg2html(self, a, prefix, defaults={}, _=lambda s:s):
name = a.option_strings[0].replace("-", "")
if isinstance(a, argparse._HelpAction):
return ""
viewname = name
if prefix and name.startswith(prefix + '_'):
viewname = name[len(prefix)+1:]
default = defaults.get(name, None)
row = """<tr><td>%s</td><td>%%s</td><td>%s</td></tr>\n""" % \
(_(viewname), "" if not a.help else _(a.help))
if (isinstance(a, argparse._StoreAction) and
hasattr(a.type, "html")):
input = a.type.html(name, default or a.default, _)
elif a.dest == "layout":
val = (default or a.default).split("\n")
input = """<textarea name="%s" cols="%s" rows="%s">%s</textarea>""" % \
(name, max((len(l) for l in val))+10, len(val)+1, default or a.default)
elif a.choices:
options = "\n".join(
("""<option value="%s"%s>%s</option>""" %
(e, ' selected="selected"' if e == (default or a.default) else "",
_(e)) for e in a.choices))
input = """<select name="%s" size="1">\n%s</select>\n""" % (name, options)
else:
input = """<input name="%s" type="text" value="%s">""" % \
(name, default or a.default)
return row % input
scripts = """
<script>
function showHide(id) {
var e = document.getElementById(id);
var h = document.getElementById("h-" + id);
if(e.style.display == null || e.style.display == "none") {
e.style.display = "block";
h.classList.add("open");
} else {
e.style.display = "none";
h.classList.remove("open");
}
}
function hideargs() {
for ( i=0; i<%i; i++) {
showHide(i);
}
}
</script>
"""
def args2html(self, name, box, lang, action="", defaults={}):
_ = lang.gettext
lang_name = lang.info().get('language', None)
if lang_name:
langparam = "?language=" + lang_name
else:
langparam = ""
result = ["""<!DOCTYPE html>
<html>
<head>
<title>""" + _("Boxes - %s") % _(name), """</title>
<meta charset="utf-8">
<link rel="icon" type="image/svg+xml" href="static/boxes-logo.svg" sizes="any">
<link rel="icon" type="image/x-icon" href="static/favicon.ico">
<link rel="stylesheet" href="static/self.css">
""", self.scripts % (len(box.argparser._action_groups)-3), """
<meta name="flattr:id" content="456799">
</head>
<body onload="hideargs()">
<div class="container" style="background-color: #FFF8EA;">
<div style="float: left;">
<a href="./""" + langparam + '"><h1>' + _("Boxes.py") + """</h1></a>
</div>
<div style="width: 120px; float: right;">
<img alt="self-Logo" src="static/boxes-logo.svg" width="120" >
</div>
<div>
<div class="clear"></div>
<hr>
<h2 style="margin: 0px 0px 0px 20px;" >""", _(name), """</h2>
<p>""", _(box.__doc__) if box.__doc__ else "", """</p>
<form action="%s" method="GET">
""" % (action)]
groupid = 0
for group in box.argparser._action_groups[3:] + box.argparser._action_groups[:3]:
if not group._group_actions:
continue
if len(group._group_actions) == 1 and isinstance(group._group_actions[0], argparse._HelpAction):
continue
prefix = getattr(group, "prefix", None)
result.append('''<h3 id="h-%s" class="open" onclick="showHide(%s)">%s</h3>\n<table id="%s">\n''' % (groupid, groupid, _(group.title), groupid))
for a in group._group_actions:
if a.dest in ("input", "output"):
continue
result.append(self.arg2html(a, prefix, defaults, _))
result.append("</table>")
groupid += 1
result.append("""
<p>
<button name="render" value="1" formtarget="_blank">""" + _("Generate") + """</button>
<button name="render" value="0" formtarget="_self">""" + _("Save to URL") + """</button>
</p>
</form>
</div>
<!--
<div style="width: 5%; float: left;"></div>
<div style="width: 35%; float: left;">
<img alt="sample" src="examples/box.svg" width="300" >
<span id="sicherheitshinweise">hier kommt dann der AJAX-Inhalt</span>
</div>
-->
<div class="clear"></div>
<hr>
<div class="description">
""")
no_img_msg = _('There is no image yet. Please donate an image of your project on <a href=&quot;https://github.com/florianfesti/boxes/issues/140&quot;>GitHub</a>!')
if box.description:
result.append(markdown.markdown(_(box.description)))
result.append(f'''<div>
<img src="static/samples/{box.__class__.__name__}.jpg" width="100%" onerror="this.parentElement.innerHTML = '{no_img_msg}';">
</div>
''')
result.append("""
</div>
</div>
""" + self.footer(lang) + """</body>
</html>
""" )
return (s.encode("utf-8") for s in result)
def menu(self, lang):
_ = lang.gettext
lang_name = lang.info().get('language', None)
if lang_name:
langparam = "?language=" + lang_name
else:
langparam = ""
result = ["""<!DOCTYPE html>
<html>
<head>
<title>""" + _("Boxes.py") + """</title>
<meta charset="utf-8">
<link rel="icon" type="image/svg+xml" href="static/boxes-logo.svg" sizes="any">
<link rel="icon" type="image/x-icon" href="static/favicon.ico">
<link rel="stylesheet" href="static/self.css">
<script>
function change(group, img_link){
document.getElementById("sample-"+group).src = img_link;
document.getElementById("sample-"+group).style.height = "auto";
}
function changeback(group){
document.getElementById("sample-" + group).src= "static/nothing.png";
document.getElementById("sample-" + group).style.height= "0px";
}
</script>""", self.scripts % len(self.groups), """
<meta name="flattr:id" content="456799">
</head>
<body onload="hideargs()">
<div class="container" style="background-color: #FFF8EA;">
<div style="width: 75%; float: left;">
<h1>""" + _("Boxes.py") + """</h1>
<p>
""" + _("Create boxes and more with a laser cutter!") + """
</p>
<p>
""" + _("""
<a href="https://hackaday.io/project/10649-boxespy">Boxes.py</a> is an <a href="https://www.gnu.org/licenses/gpl-3.0.en.html">Open Source</a> box generator written in <a href="https://www.python.org/">Python</a>. It features both finished parametrized generators as well as a Python API for writing your own. It features finger and (flat) dovetail joints, flex cuts, holes and slots for screws, hinges, gears, pulleys and much more.""") + """
</p>
</div>
<div style="width: 25%; float: left;">
<img alt="self-Logo" src="static/boxes-logo.svg" width="250" >
</div>
<div>
<div class="clear"></div>
<hr>
<div class="menu" style="width: 100%">
""" ]
for nr, group in enumerate(self.groups):
result.append(f'''
<h3 id="h-{nr}" class="open" onclick="showHide('{nr}')"
onmouseenter="change('{group.name}', 'static/samples/{group.thumbnail}')"
onmouseleave="changeback('{group.name}')">{_(group.title)}</h3>
<img style="width: 200px;" id="sample-{group.name}" src="static/nothing.png" alt="">
<div id="{nr}"><ul>''')
for box in group.generators:
name = box.__name__
if name in ("TrayLayout2", ):
continue
docs = ""
if box.__doc__:
docs = " - " + _(box.__doc__)
result.append(f"""
<li onmouseenter="change('{group.name}', 'static/samples/{name}-thumb.jpg')" onmouseleave="changeback('{group.name}')"><a href="{name}{langparam}">{_(name)}</a>{docs}</li>""")
result.append("""
</ul></div>
""")
result.append("""
</div>
<div style="width: 5%; float: left;"></div>
<div class="clear"></div>
<hr>
</div>
</div>""" + self.footer(lang) + """
</body>
</html>
""")
return (s.encode("utf-8") for s in result)
def footer(self, lang):
_ = lang.gettext
language = lang.info().get('language', '')
return """
<div class="footer container">
<ul>
<li><form><select name="language" onchange='if(this.value != "%s") { this.form.submit(); }'>""" % language + \
("<option value='' selected></option>" if not language else "") + \
"\n".join(
("<option value='%s' %s>%s</option>" % (l, "selected" if l==language else "", l)
for l in self.getLanguages())) + """
</select></form></li>
<li><a href="https://github.com/florianfesti/boxes">""" + _("Get Source at GitHub") + """</a></li>
<li><a href="https://florianfesti.github.io/boxes/html/index.html">""" + _("Documentation and API Description") + """</a></li>
<li><a href="https://hackaday.io/project/10649-boxespy">""" + _("Hackaday.io Project Page") + """</a></li>
</ul>
</div>
"""
def errorMessage(self, name, e, _):
return [
("""<html>
<head>
<title>""" + _("Error generating %s") % _(name) +
"""</title>
<meta name="flattr:id" content="456799">
</head>
<body>
<h1>""" + _("An error occurred!") + "</h1>" +
"".join(u"<p>%s</p>" % html.escape(s) for s in type(u"")(e).split(u"\n")) +
"""
</body>
</html>
""").encode("utf-8") ]
def serveStatic(self, environ, start_response):
filename = environ["PATH_INFO"][len("/static/"):]
path = os.path.join(self.staticdir, filename)
if (not re.match(r"[a-zA-Z0-9_/-]+\.[a-zA-Z0-9]+", filename) or
not os.path.exists(path)):
if re.match(r"samples/.*-thumb.jpg", filename):
path = os.path.join(self.staticdir, "nothing.png")
else:
start_response("404 Not Found",
[('Content-type', 'text/plain')])
return [b"Not found"]
type_, encoding = mimetypes.guess_type(filename)
if encoding is None:
encoding = "utf-8"
# Images do not have charset. Just bytes. Except text based svg.
# Todo: fallback if type_ is None?
if type_ is not None and "image" in type_ and type_ != "image/svg+xml":
start_response("200 OK", [('Content-type', "%s" % type_)])
else:
start_response("200 OK", [('Content-type', "%s; charset=%s" % (type_, encoding))])
f = open(path, 'rb')
return environ['wsgi.file_wrapper'](f, 512*1024)
def getURL(self, environ):
url = environ['wsgi.url_scheme']+'://'
if environ.get('HTTP_HOST'):
url += environ['HTTP_HOST']
else:
url += environ['SERVER_NAME']
if environ['wsgi.url_scheme'] == 'https':
if environ['SERVER_PORT'] != '443':
url += ':' + environ['SERVER_PORT']
else:
if environ['SERVER_PORT'] != '80':
url += ':' + environ['SERVER_PORT']
url += quote(environ.get('SCRIPT_NAME', ''))
url += quote(environ.get('PATH_INFO', ''))
if environ.get('QUERY_STRING'):
url += '?' + environ['QUERY_STRING']
return url
def serve(self, environ, start_response):
if environ["PATH_INFO"].startswith("/static/"):
return self.serveStatic(environ, start_response)
status = '200 OK'
headers = [('Content-type', 'text/html; charset=utf-8'), ('X-XSS-Protection', '1; mode=block'), ('X-Content-Type-Options', 'nosniff'), ('x-frame-options', 'SAMEORIGIN'), ('Referrer-Policy', 'no-referrer')]
d = parse_qs(environ.get('QUERY_STRING', ''))
name = environ["PATH_INFO"][1:]
args = [unquote_plus(arg) for arg in
environ.get('QUERY_STRING', '').split("&")]
lang = self.getLanguage(args, environ.get("HTTP_ACCEPT_LANGUAGE", ""))
_ = lang.gettext
box_cls = self.boxes.get(name, None)
if not box_cls:
start_response(status, headers)
return self.menu(lang)
if name == "TrayLayout2":
box = box_cls(self, webargs=True)
else:
box = box_cls()
if "render=1" not in args:
defaults = { }
for a in args:
kv = a.split('=')
if len(kv) == 2:
k, v = kv
defaults[k] = html.escape(v, True)
start_response(status, headers)
return self.args2html(name, box, lang, "./" + name, defaults=defaults)
else:
args = ["--"+ arg for arg in args if arg != "render=1"]
try:
box.parseArgs(args)
except (ArgumentParserError) as e:
start_response(status, headers)
return self.errorMessage(name, e, _)
if name == "TrayLayout":
start_response(status, headers)
box.fillDefault(box.x, box.y)
layout2 = boxes.generators.traylayout.TrayLayout2(self, webargs=True)
layout2.argparser.set_defaults(layout=str(box))
return self.args2html(
name, layout2, lang, action="TrayLayout2")
if name == "TrayLayout2":
try:
box.parse(box.layout.split("\n"))
except Exception as e:
start_response(status, headers)
return self.errorMessage(name, e, _)
try:
fd, box.output = tempfile.mkstemp()
box.metadata["url"] = self.getURL(environ)
box.open()
box.render()
box.close()
except Exception as e:
if not isinstance(e, ValueError):
print("Exception during rendering:")
traceback.print_exc()
start_response("500 Internal Server Error",
headers)
return self.errorMessage(name, e, _)
http_headers = box.formats.http_headers.get(
box.format,
[('Content-type', 'application/unknown; charset=utf-8')])[:]
if box.format != "svg":
extension = box.format
if extension == "svg_Ponoko":
extension = "svg"
http_headers.append(('Content-Disposition', 'attachment; filename="%s.%s"' % (box.__class__.__name__, extension)))
start_response(status, http_headers)
result = open(box.output, 'rb').readlines()
os.close(fd)
os.remove(box.output)
return (l for l in result)
if __name__=="__main__":
if len(sys.argv) > 1:
port = int(sys.argv[1])
else:
port = 8000
fc = FileChecker()
fc.start()
boxserver = BServer()
httpd = make_server('', port, boxserver.serve)
print("BoxesServer serving on port %s..." %port)
httpd.serve_forever()
else:
application = BServer().serve