#!/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 . 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 = {} self._stopped = False 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 not self._stopped: if not self.filesOK(): os.execv(__file__, sys.argv) time.sleep(1) def stop(self): self._stopped = True 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 = """%%s%s\n""" % \ (name+"_id", name, _(viewname), name+"_description","" if not a.help else markdown.markdown(_(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 = """""" % \ (name, name, name+"_id", name+"_description", max((len(l) for l in val))+10, len(val)+1, default or a.default) elif a.choices: options = "\n".join( ("""""" % (e, ' selected="selected"' if (e == (default or a.default)) or (str(e) == str(default or a.default)) else "", _(e)) for e in a.choices)) input = """\n""" % (name, name, name+"_id", name+"_description", options) else: input = """""" % \ (name, name, name+"_id", name+"_description", default or a.default) return row % input scripts = """ """ 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 = [""" """ + _("Boxes - %s") % _(name), """ """, self.scripts % (len(box.argparser._action_groups)-3), """

' + _("Boxes.py") + """

self-Logo

""", _(name), """

""", _(box.__doc__) if box.__doc__ else "", """

""" % (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('''

%s

\n\n''' % (groupid, 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("") groupid += 1 result.append("""


""") no_img_msg = _('There is no image yet. Please donate an image of your project on GitHub!') if box.description: result.append(markdown.markdown(_(box.description), extensions=["extra"])) result.append(f'''
''') result.append("""
""" + self.footer(lang) + """ """ ) 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 = [""" """ + _("Boxes.py") + """ """, self.scripts % len(self.groups), """

""" + _("Boxes.py") + """

""" + _("Create boxes and more with a laser cutter!") + """

""" + _(""" Boxes.py is an Open Source box generator written in Python. 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.""") + """

self-Logo


""" + self.footer(lang) + """ """) return (s.encode("utf-8") for s in result) def footer(self, lang): _ = lang.gettext language = lang.info().get('language', '') return """ """ def errorMessage(self, name, e, _): return [ (""" """ + _("Error generating %s") % _(name) + """

""" + _("An error occurred!") + "

" + "".join(u"

%s

" % html.escape(s) for s in type(u"")(e).split(u"\n")) + """ """).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__": host = '' port = 8000 if len(sys.argv) > 1: tmp = sys.argv[1].split(':') if len(tmp) == 2: host = tmp[0] port = int(tmp[1]) else: port = int(tmp[0]) fc = FileChecker() fc.start() boxserver = BServer() httpd = make_server(host, port, boxserver.serve) print("BoxesServer serving on host:port %s:%s..." % (host, port) ) try: httpd.serve_forever() except KeyboardInterrupt: fc.stop() httpd.server_close() print("BoxesServer stops.") else: application = BServer().serve