#!/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 argparse import gettext import glob import html import mimetypes import os.path import re import sys import tempfile import threading import time import traceback from urllib.parse import parse_qs from urllib.parse import unquote_plus, quote from wsgiref.simple_server import make_server import markdown 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) # Evil hack boxes.ArgumentParser = ThrowingArgumentParser # type: ignore static_url = "static" 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 self._cache = {} 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_cached(self, name, box, lang, action="", defaults={}): if defaults == {}: key = (name, lang.info().get('language', None), action) if key not in self._cache: self._cache[key] = list(self.args2html(name, box, lang, action, defaults)) return self._cache[key] return self.args2html(name, box, lang, action, defaults) 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 = [f""" {_("Boxes - %s") % _(name)} {self.scripts % (len(box.argparser._action_groups)-3)}

{_("Boxes.py")}

self-Logo

{_(name)}

{_(box.__doc__) if box.__doc__ else ""}

"""] 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(f'''

{_(group.title)}

\n\n''') 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(f"""


""") 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"]) .replace('src="static/', f'src="{static_url}/')) result.append(f'''
{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 = [f""" {_("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 [(f""" {_("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("&")] render = "0" for arg in args: if arg.startswith("render="): render = arg[len("render="):] 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) lang_name = lang.info().get('language', None) if lang_name not in self._cache: self._cache[lang_name] = list(self.menu(lang)) return self._cache[lang_name] if name == "TrayLayout2": box = box_cls(self, webargs=True) else: box = box_cls() if render == "0": 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_cached(name, box, lang, "./" + name, defaults=defaults) else: args = ["--"+ arg for arg in args if not arg.startswith("render=")] 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" or render == "2": 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 static_url = "https://florianfesti.github.io/boxes/static"