diff options
| -rw-r--r-- | config | 4 | ||||
| -rw-r--r-- | module/web/WebServer.py | 428 | ||||
| -rw-r--r-- | module/web/bottle.py | 727 | ||||
| -rwxr-xr-x | pyLoadCore.py | 57 | 
4 files changed, 929 insertions, 287 deletions
| @@ -5,12 +5,12 @@ username = admin  password = pwhere  [ssl] -activated = True +activated = False  cert = ssl.crt  key = ssl.key  [webinterface] -activated = False  +activated = True   listenaddr = 0.0.0.0  port = 8080  username = User diff --git a/module/web/WebServer.py b/module/web/WebServer.py index 0712f1dce..29b0aafe8 100644 --- a/module/web/WebServer.py +++ b/module/web/WebServer.py @@ -1,72 +1,364 @@ -import sys -from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler -from xmlrpclib import ServerProxy -from time import time -import re +#import sys +#from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler +#from xmlrpclib import ServerProxy +#from time import time +#import re +# +#class Handler(BaseHTTPRequestHandler): +#     +#    def do_GET(self): +#        global coreserver +#        stdout = sys.stdout +#        sys.stdout = self.wfile +#        if self.path == "/": +#            print "Server Runs" +#        elif self.path == "/downloads": +#            print self.get_downloads() +#        elif re.search("/add=.?", self.path): +#            if re.match(is_url, self.path.split("/add=")[1]): +#                coreserver.add_urls([self.path.split("/add=")[1]]) +#                print "Link Added" +#        else: +#            try:  +#                print open(self.path[1:], 'r').read() +#            except IOError: +#                self.send_error(404) +#                 +#    def format_size(self, size): +#        return str(size / 1024) + " MiB" +# +#    def format_time(self,seconds): +#        seconds = int(seconds) +#        hours, seconds = divmod(seconds, 3600) +#        minutes, seconds = divmod(seconds, 60) +#        return "%.2i:%.2i:%.2i" % (hours, minutes, seconds) +#                 +#    def get_downloads(self): +#        data = coreserver.status_downloads() +#        for download in data: +#            print "<h3>%s</h3>" % download["name"] +#            if download["status"] == "downloading": +#                percent = download["percent"] +#                z = percent / 4 +#                print "<h3>%s</h3>" % dl_name +#                print "<font face='font-family:Fixedsys,Courier,monospace;'>[" + z * "#" + (25-z) * " " + "]</font>" + str(percent) + "%<br />" +#                print "Speed: " + str(int(download['speed'])) + " kb/s" +#                print "Size: " + self.format_size(download['size']) +#                print "Finished in: " + self.format_time(download['eta']) +#                print "ID: " + str(download['id']) +#                dl_status = "[" + z * "#" + (25-z) * " " + "] " + str(percent) + "%" + " Speed: " + str(int(download['speed'])) + " kb/s" + " Size: " + self.format_size(download['size']) + " Finished in: " + self.format_time(download['eta'])  + " ID: " + str(download['id']) +#            if download["status"] == "waiting": +#                print "waiting: " + self.format_time(download["wait_until"]- time()) +#                 +#is_url = re.compile("^(((https?|ftp)\:\/\/)?([\w\.\-]+(\:[\w\.\&%\$\-]+)*@)?((([^\s\(\)\<\>\\\"\.\[\]\,@;:]+)(\.[^\s\(\)\<\>\\\"\.\[\]\,@;:]+)*(\.[a-zA-Z]{2,4}))|((([01]?\d{1,2}|2[0-4]\d|25[0-5])\.){3}([01]?\d{1,2}|2[0-4]\d|25[0-5])))(\b\:(6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]\d{4}|[1-9]\d{0,3}|0)\b)?((\/[^\/][\w\.\,\?\'\\\/\+&%\$#\=~_\-@]*)*[^\.\,\?\"\'\(\)\[\]!;<>{}\s\x7F-\xFF])?)$",re.IGNORECASE) +# +#coreserver = None +# +#class WebServer(): +# +#    def start(self): +#        try: +#            global coreserver +#            coreserver = ServerProxy("https://testuser:testpw@localhost:1337", allow_none=True) +#            webserver = HTTPServer(('',8080),Handler) +#            print 'server started at port 8080' +#            webserver.serve_forever() +#        except KeyboardInterrupt: +#            webserver.socket.close() +# +#if __name__ == "__main__": +#    web = WebServer() +#    web.start() -class Handler(BaseHTTPRequestHandler): + +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +#Copyright (C) 2009 RaNaN +# +#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 random +import threading +import time + +import bottle +from bottle import abort +from bottle import redirect +from bottle import request +from bottle import response +from bottle import route +from bottle import run +from bottle import send_file +from bottle import template +from bottle import validate + + +core = None +core_methods = None + +PATH = "./module/web/" +TIME = time.strftime("%a, %d %b %Y 00:00:00 +0000", time.localtime()) #set time to current day +USERS = {} +# TODO: Implement new server methods +@route('/login', method='POST') +def do_login(): +    #print request.GET + + +    username = core.config['webinterface']['username'] +    pw = core.config['webinterface']['password'] + +    if request.POST['u'] == username and request.POST['p'] == pw: +         +        id = int(random.getrandbits(16)) +        ua = request.HEADER("HTTP_USER_AGENT") +        ip = request.HEADER("REMOTE_ADDR") + +        auth = {} + +        auth['ua'] = ua +        auth['ip'] = ip +        auth['user'] = username + +        USERS[id] = auth + +        response.COOKIES['user'] = username +        response.COOKIES['id'] = id + +        return template('default', page='loggedin', user=username) +    else: +        return template('default', page='login') + +@route('/login') +def login(): + +    if check_auth(request): +        redirect("/") + +    return template('default', page='login') + +@route('/logout') +def logout(): +    try: +        del USERS[int(request.COOKIES.get('id'))] +    except: +        pass -    def do_GET(self): -        global coreserver -        stdout = sys.stdout -        sys.stdout = self.wfile -        if self.path == "/": -            print "Server Runs" -        elif self.path == "/downloads": -            print self.get_downloads() -        elif re.search("/add=.?", self.path): -            if re.match(is_url, self.path.split("/add=")[1]): -                coreserver.add_urls([self.path.split("/add=")[1]]) -                print "Link Added" +    redirect("/login") + +@route('/') +def home(): + +    if not check_auth(request): +        redirect("/login") + +    username = request.COOKIES.get('user') + +    dls = core_methods.status_downloads() + +    for dl in dls: +        dl['eta'] = str(core.format_time(dl['eta'])) +        dl['wait_until'] = str(core.format_time(dl['wait_until'] - time.time())) + +         +    return template('default', page='home', links=dls, user=username, status=core_methods.status_server()) + +@route('/queue') +def queue(): + +    if not check_auth(request): +        redirect("/login") + +    username = request.COOKIES.get('user') + +    return template('default', page='queue', links=core.get_links(), user=username, status=core_methods.status_server()) + +@route('/downloads') +def downloads(): + +    if not check_auth(request): +        redirect("/login") + +    username = request.COOKIES.get('user') + +    return template('default', page='downloads', links=core_methods.status_downloads(), user=username, status=core_methods.status_server()) + + +@route('/logs') +def logs(): + +    if not check_auth(request): +        redirect("/login") + +    username = request.COOKIES.get('user') + +    return template('default', page='logs', links=core_methods.status_downloads(), user=username, status=core_methods.status_server()) + +@route('/json/links') +def get_links(): +    response.header['Cache-Control'] = 'no-cache, must-revalidate' +    response.content_type = 'application/json' + +    if not check_auth(request): +        abort(404, "No Access") + +    json = '{ "downloads": [' + +    downloads = core_methods.status_downloads() +    ids = [] + +    for dl in downloads: +        ids.append(dl['id']) +        json += '{' +        json += '"id": %s, "name": "%s", "speed": %s, "eta": "%s", "kbleft": %s, "size": %s, "percent": %s, "wait": "%s", "status": "%s"'\ +            % (str(dl['id']), str(dl['name']), str(int(dl['speed'])), str(core.format_time(dl['eta'])), dl['kbleft'], dl['size'], dl['percent'], str(core.format_time(dl['wait_until'] - time.time())), dl['status']) + +        json += "}," + +    if json.endswith(","): json = json[:-1] + +    json += '], "ids": %s }' % str(ids) + +    return json + +@route('/json/status') +def get_status(): +    response.header['Cache-Control'] = 'no-cache, must-revalidate' +    response.content_type = 'application/json' + +    if not check_auth(request): +        abort(404, "No Access") + +    data = core_methods.status_server() + +    if data['pause']: +        status = "paused" +    else: +        status = "running" + +    json = '{ "status": "%s", "speed": "%s", "queue": "%s" }' % (status, str(int(data['speed'])), str(data['queue'])) + +    return json + +@route('/json/addlinks', method='POST') +def add_links(): +    response.header['Cache-Control'] = 'no-cache, must-revalidate' +    response.content_type = 'application/json' + +    if not check_auth(request): +        abort(404, "No Access") + +    links = request.POST['links'].split('\n') + +    core.add_links(links) +     +    return "{}" + +@route('/json/pause') +def pause(): +    response.header['Cache-Control'] = 'no-cache, must-revalidate' +    response.content_type = 'application/json' + +    if not check_auth(request): +        abort(404, "No Access") + +    core.thread_list.pause = True + +    return "{}" + + +@route('/json/play') +def play(): +    response.header['Cache-Control'] = 'no-cache, must-revalidate' +    response.content_type = 'application/json' + +    if not check_auth(request): +        abort(404, "No Access") + +    core.thread_list.pause = False + +    return "{}" + +@route('/favicon.ico') +def favicon(): +     +    if request.HEADER("HTTP_IF_MODIFIED_SINCE") == TIME: abort(304, "Not Modified") + +    response.header['Last-Modified'] = TIME + +    send_file('favicon.ico', root=(PATH + 'static/')) + +@route('static/:section/:filename') +def static_folder(section, filename): +     +    if request.HEADER("HTTP_IF_MODIFIED_SINCE") == TIME: abort(304, "Not Modified") + +    response.header['Last-Modified'] = TIME +    send_file(filename, root=(PATH + 'static/' + section)) + +@route('/static/:filename') +def static_file(filename): + +    if request.HEADER("HTTP_IF_MODIFIED_SINCE") == TIME: abort(304, "Not Modified") +     +    response.header['Last-Modified'] = TIME +    send_file(filename, root=(PATH + 'static/')) + + +def check_auth(req): + +    try: +        user = req.COOKIES.get('user') +        id = int(req.COOKIES.get('id')) +        ua = req.HEADER("HTTP_USER_AGENT") +        ip = req.HEADER("REMOTE_ADDR") + +        if USERS[id]['user'] == user and USERS[id]['ua'] == ua and USERS[id]['ip'] == ip: +            return True +    except: +        return False + +    return False + + +class WebServer(threading.Thread): +    def __init__(self, pycore): +        threading.Thread.__init__(self) + +        global core, core_methods, TIME +        core = pycore +        core_methods = pycore.server_methods +        self.core = pycore +        self.setDaemon(True) +         +        if pycore.config['general']['debug_mode']: +            bottle.debug(True) +            TIME = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.localtime())          else: -            try:  -                print open(self.path[1:], 'r').read() -            except IOError: -                self.send_error(404) -                 -    def format_size(self, size): -        return str(size / 1024) + " MiB" - -    def format_time(self,seconds): -        seconds = int(seconds) -        hours, seconds = divmod(seconds, 3600) -        minutes, seconds = divmod(seconds, 60) -        return "%.2i:%.2i:%.2i" % (hours, minutes, seconds) -                 -    def get_downloads(self): -        data = coreserver.status_downloads() -        for download in data: -            print "<h3>%s</h3>" % download["name"] -            if download["status"] == "downloading": -                percent = download["percent"] -                z = percent / 4 -                print "<h3>%s</h3>" % dl_name -                print "<font face='font-family:Fixedsys,Courier,monospace;'>[" + z * "#" + (25-z) * " " + "]</font>" + str(percent) + "%<br />" -                print "Speed: " + str(int(download['speed'])) + " kb/s" -                print "Size: " + self.format_size(download['size']) -                print "Finished in: " + self.format_time(download['eta']) -                print "ID: " + str(download['id']) -                dl_status = "[" + z * "#" + (25-z) * " " + "] " + str(percent) + "%" + " Speed: " + str(int(download['speed'])) + " kb/s" + " Size: " + self.format_size(download['size']) + " Finished in: " + self.format_time(download['eta'])  + " ID: " + str(download['id']) -            if download["status"] == "waiting": -                print "waiting: " + self.format_time(download["wait_until"]- time()) -                 -is_url = re.compile("^(((https?|ftp)\:\/\/)?([\w\.\-]+(\:[\w\.\&%\$\-]+)*@)?((([^\s\(\)\<\>\\\"\.\[\]\,@;:]+)(\.[^\s\(\)\<\>\\\"\.\[\]\,@;:]+)*(\.[a-zA-Z]{2,4}))|((([01]?\d{1,2}|2[0-4]\d|25[0-5])\.){3}([01]?\d{1,2}|2[0-4]\d|25[0-5])))(\b\:(6553[0-5]|655[0-2]\d|65[0-4]\d{2}|6[0-4]\d{3}|[1-5]\d{4}|[1-9]\d{0,3}|0)\b)?((\/[^\/][\w\.\,\?\'\\\/\+&%\$#\=~_\-@]*)*[^\.\,\?\"\'\(\)\[\]!;<>{}\s\x7F-\xFF])?)$",re.IGNORECASE) - -coreserver = None - -class WebServer(): - -    def start(self): -        try: -            global coreserver -            coreserver = ServerProxy("https://testuser:testpw@localhost:1337", allow_none=True) -            webserver = HTTPServer(('',8080),Handler) -            print 'server started at port 8080' -            webserver.serve_forever() -        except KeyboardInterrupt: -            webserver.socket.close() - -if __name__ == "__main__": -    web = WebServer() -    web.start() +            bottle.debug(False) + +        #@TODO remove +        #TIME = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.localtime()) +        bottle.TEMPLATE_PATH.append('./module/web/templates/') +     +    def run(self): +        self.core.logger.info("Starting Webinterface on %s port %s" % (self.core.config['webinterface']['listenaddr'],self.core.config['webinterface']['port'])) +        try: +            run(host=self.core.config['webinterface']['listenaddr'], port=int(self.core.config['webinterface']['port']), quiet=True) +        except: +            self.core.logger.error("Failed starting webserver, no webinterface available: Can't create socket") +            exit()
\ No newline at end of file diff --git a/module/web/bottle.py b/module/web/bottle.py index 66ceb527f..41a8c8fc0 100644 --- a/module/web/bottle.py +++ b/module/web/bottle.py @@ -1,15 +1,15 @@  # -*- coding: utf-8 -*-  """ -Bottle is a fast and simple mirco-framework for small web-applications. It -offers request dispatching (Routes) with url parameter support, Templates, -key/value Databases, a build-in HTTP Server and adapters for many third party -WSGI/HTTP-server and template engines. All in a single file and with no +Bottle is a fast and simple micro-framework for small web applications. It +offers request dispatching (Routes) with url parameter support, templates, +key/value databases, a built-in HTTP Server and adapters for many third party +WSGI/HTTP-server and template engines - all in a single file and with no  dependencies other than the Python Standard Library.  Homepage and documentation: http://wiki.github.com/defnull/bottle  Special thanks to Stefan Matthias Aust [http://github.com/sma] -  for his contribution to SimpelTemplate +  for his contribution to SimpleTemplate  Licence (MIT)  ------------- @@ -62,9 +62,10 @@ Example  """  __author__ = 'Marcel Hellkamp' -__version__ = '0.5.7' +__version__ = '0.6.4'  __license__ = 'MIT' +import types  import sys  import cgi  import mimetypes @@ -75,26 +76,31 @@ import re  import random  import threading  import time +import warnings +import email.utils  from wsgiref.headers import Headers as HeaderWrapper  from Cookie import SimpleCookie  import anydbm as dbm +import subprocess +import thread +  try:      from urlparse import parse_qs -except ImportError: +except ImportError: # pragma: no cover      from cgi import parse_qs  try:      import cPickle as pickle -except ImportError: +except ImportError: # pragma: no cover      import pickle as pickle  try:      try:          from json import dumps as json_dumps -    except ImportError: +    except ImportError: # pragma: no cover          from simplejson import dumps as json_dumps  -except ImportError: +except ImportError: # pragma: no cover      json_dumps = None @@ -105,38 +111,37 @@ except ImportError:  # Exceptions and Events  class BottleException(Exception): -    """ A base class for exceptions used by bottle.""" +    """ A base class for exceptions used by bottle. """      pass  class HTTPError(BottleException): -    """ A way to break the execution and instantly jump to an error handler. """ +    """ +    A way to break the execution and instantly jump to an error handler. +    """      def __init__(self, status, text):          self.output = text          self.http_status = int(status) +        BottleException.__init__(self, status, text)      def __repr__(self): -        return "HTTPError(%d,%s)" % (self.http_status, repr(self.output)) +        return 'HTTPError(%d,%s)' % (self.http_status, repr(self.output))      def __str__(self): -        out = [] -        status = self.http_status -        name = HTTP_CODES.get(status,'Unknown').title() -        url = request.path -        out.append('<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">') -        out.append('<html><head><title>Error %d: %s</title>' % (status, name)) -        out.append('</head><body><h1>Error %d: %s</h1>' % (status, name)) -        out.append('<p>Sorry, the requested URL "%s" caused an error.</p>' % url) -        out.append(''.join(list(self.output))) -        out.append('</body></html>') -        return "\n".join(out) +        return HTTP_ERROR_TEMPLATE % { +            'status' : self.http_status, +            'url' : request.path, +            'error_name' : HTTP_CODES.get(self.http_status, 'Unknown').title(), +            'error_message' : ''.join(self.output) +        }  class BreakTheBottle(BottleException): -    """ Not an exception, but a straight jump out of the controller code. -     +    """ +    Not an exception, but a straight jump out of the controller code.      Causes the Bottle to instantly call start_response() and return the -    content of output """ +    content of output +    """      def __init__(self, output):          self.output = output @@ -149,8 +154,10 @@ class BreakTheBottle(BottleException):  _default_app = None  def default_app(newapp = None): -    ''' Returns the current default app or sets a new one. -        Defaults to an instance of Bottle ''' +    """ +    Returns the current default app or sets a new one. +    Defaults to an instance of Bottle +    """      global _default_app      if newapp:          _default_app = newapp @@ -161,17 +168,20 @@ def default_app(newapp = None):  class Bottle(object): -    def __init__(self, catchall=True, debug=False, optimize=False, autojson=True): +    def __init__(self, catchall=True, optimize=False, autojson=True):          self.simple_routes = {}          self.regexp_routes = {} +        self.default_route = None          self.error_handler = {}          self.optimize = optimize -        self.debug = debug          self.autojson = autojson          self.catchall = catchall +        self.serve = True      def match_url(self, url, method='GET'): -        """Returns the first matching handler and a parameter dict or (None, None) """ +        """ +        Returns the first matching handler and a parameter dict or (None, None) +        """          url = url.strip().lstrip("/ ")          # Search for static routes first          route = self.simple_routes.get(method,{}).get(url,None) @@ -186,47 +196,116 @@ class Bottle(object):                  if i > 0 and self.optimize and random.random() <= 0.001:                      routes[i-1], routes[i] = routes[i], routes[i-1]                  return (handler, match.groupdict()) -        return (None, None) - -    def add_route(self, route, handler, method='GET', simple=False): +        if self.default_route: +            return (self.default_route, {}) +        if method == 'HEAD': # Fall back to GET +            return self.match_url(url) +        else: +            return (None, None) + +    def add_controller(self, route, controller, **kargs): +        """ Adds a controller class or object """ +        if '{action}' not in route and 'action' not in kargs: +            raise BottleException("Routes to controller classes or object MUST" +                " contain an {action} placeholder or use the action-parameter") +        for action in (m for m in dir(controller) if not m.startswith('_')): +            handler = getattr(controller, action) +            if callable(handler) and action == kargs.get('action', action): +                self.add_route(route.replace('{action}', action), handler, **kargs) + +    def add_route(self, route, handler, method='GET', simple=False, **kargs):          """ Adds a new route to the route mappings. """ +        if isinstance(handler, type) and issubclass(handler, BaseController): +            handler = handler() +        if isinstance(handler, BaseController): +            self.add_controller(route, handler, method=method, simple=simple, **kargs) +            return          method = method.strip().upper()          route = route.strip().lstrip('$^/ ').rstrip('$^ ')          if re.match(r'^(\w+/)*\w*$', route) or simple:              self.simple_routes.setdefault(method, {})[route] = handler          else: -            route = re.sub(r':([a-zA-Z_]+)(?P<uniq>[^\w/])(?P<re>.+?)(?P=uniq)',r'(?P<\1>\g<re>)',route) -            route = re.sub(r':([a-zA-Z_]+)',r'(?P<\1>[^/]+)', route) +            route = re.sub(r':([a-zA-Z_]+)(?P<uniq>[^\w/])(?P<re>.+?)(?P=uniq)', +                           r'(?P<\1>\g<re>)',route) +            route = re.sub(r':([a-zA-Z_]+)', r'(?P<\1>[^/]+)', route)              route = re.compile('^%s$' % route)              self.regexp_routes.setdefault(method, []).append([route, handler])      def route(self, url, **kargs): -        """ Decorator for request handler. Same as add_route(url, handler, **kargs).""" +        """ +        Decorator for request handler. +        Same as add_route(url, handler, **kargs). +        """          def wrapper(handler):              self.add_route(url, handler, **kargs)              return handler          return wrapper +    def set_default(self, handler): +        self.default_route = handler + +    def default(self): +        """ Decorator for request handler. Same as add_defroute( handler ).""" +        def wrapper(handler): +            self.set_default(handler) +            return handler +        return wrapper +      def set_error_handler(self, code, handler):          """ Adds a new error handler. """ -        code = int(code) -        self.error_handler[code] = handler +        self.error_handler[int(code)] = handler      def error(self, code=500): -        """ Decorator for error handler. Same as set_error_handler(code, handler).""" +        """ +        Decorator for error handler. +        Same as set_error_handler(code, handler). +        """          def wrapper(handler):              self.set_error_handler(code, handler)              return handler          return wrapper +    def cast(self, out): +        """ +        Cast the output to an iterable of strings or something WSGI can handle. +        Set Content-Type and Content-Length when possible. Then clear output +        on HEAD requests. +        Supports: False, str, unicode, list(unicode), dict(), open() +        """ +        if not out: +            out = [] +            response.header['Content-Length'] = '0' +        elif isinstance(out, types.StringType): +            out = [out] +        elif isinstance(out, unicode): +            out = [out.encode(response.charset)] +        elif isinstance(out, list) and isinstance(out[0], unicode): +            out = map(lambda x: x.encode(response.charset), out) +        elif self.autojson and json_dumps and isinstance(out, dict): +            out = [json_dumps(out)] +            response.content_type = 'application/json' +        elif hasattr(out, 'read'): +            out = request.environ.get('wsgi.file_wrapper', +                  lambda x: iter(lambda: x.read(8192), ''))(out) +        if isinstance(out, list) and len(out) == 1: +            response.header['Content-Length'] = str(len(out[0])) +        if not hasattr(out, '__iter__'): +            raise TypeError('Request handler for route "%s" returned [%s] ' +            'which is not iterable.' % (request.path, type(out).__name__)) +        return out + +      def __call__(self, environ, start_response): -        """ The bottle WSGI-interface .""" +        """ The bottle WSGI-interface. """          request.bind(environ)          response.bind()          try: # Unhandled Exceptions              try: # Bottle Error Handling +                if not self.serve: +                    abort(503, "Server stopped")                  handler, args = self.match_url(request.path, request.method) -                if not handler: raise HTTPError(404, "Not found") +                if not handler: +                    raise HTTPError(404, "Not found")                  output = handler(**args)                  db.close()              except BreakTheBottle, e: @@ -234,26 +313,18 @@ class Bottle(object):              except HTTPError, e:                  response.status = e.http_status                  output = self.error_handler.get(response.status, str)(e) -            # output casting -            if hasattr(output, 'read'): -                output = environ.get('wsgi.file_wrapper', lambda x: iter(lambda: x.read(8192), ''))(output) -            elif self.autojson and json_dumps and isinstance(output, dict): -                output = json_dumps(output) -                response.content_type = 'application/json' -            if isinstance(output, str): -                response.header['Content-Length'] = str(len(output)) -                output = [output] +            output = self.cast(output) +            if response.status in (100, 101, 204, 304) or request.method == 'HEAD': +                output = [] # rfc2616 section 4.3          except (KeyboardInterrupt, SystemExit, MemoryError):              raise          except Exception, e:              response.status = 500              if self.catchall:                  err = "Unhandled Exception: %s\n" % (repr(e)) -                if self.debug: -                    err += "<h2>Traceback:</h2>\n<pre>\n" -                    err += traceback.format_exc(10) -                    err += "\n</pre>" -                output = str(HTTPError(500, err)) +                if DEBUG: +                    err += TRACEBACK_TEMPLATE % traceback.format_exc(10) +                output = [str(HTTPError(500, err))]                  request._environ['wsgi.errors'].write(err)              else:                  raise @@ -267,8 +338,11 @@ class Request(threading.local):      """ Represents a single request using thread-local namespace. """      def bind(self, environ): -        """ Binds the enviroment of the current request to this request handler """ +        """ +        Binds the enviroment of the current request to this request handler +        """          self._environ = environ +        self.environ = self._environ          self._GET = None          self._POST = None          self._GETPOST = None @@ -279,25 +353,25 @@ class Request(threading.local):      @property      def method(self): -        ''' Returns the request method (GET,POST,PUT,DELETE,...) ''' +        """ Get the request method (GET,POST,PUT,DELETE,...) """          return self._environ.get('REQUEST_METHOD', 'GET').upper()      @property      def query_string(self): -        ''' Content of QUERY_STRING ''' +        """ Get content of QUERY_STRING """          return self._environ.get('QUERY_STRING', '')      @property      def input_length(self): -        ''' Content of CONTENT_LENGTH ''' +        """ Get content of CONTENT_LENGTH """          try: -            return int(self._environ.get('CONTENT_LENGTH', '0')) +            return max(0,int(self._environ.get('CONTENT_LENGTH', '0')))          except ValueError:              return 0      @property      def GET(self): -        """Returns a dict with GET parameters.""" +        """ Get a dict with GET parameters. """          if self._GET is None:              data = parse_qs(self.query_string, keep_blank_values=True)              self._GET = {} @@ -310,9 +384,10 @@ class Request(threading.local):      @property      def POST(self): -        """Returns a dict with parsed POST or PUT data.""" +        """ Get a dict with parsed POST or PUT data. """          if self._POST is None: -            data = cgi.FieldStorage(fp=self._environ['wsgi.input'], environ=self._environ, keep_blank_values=True) +            data = cgi.FieldStorage(fp=self._environ['wsgi.input'], +                environ=self._environ, keep_blank_values=True)              self._POST  = {}              for item in data.list:                  name = item.name @@ -326,7 +401,7 @@ class Request(threading.local):      @property      def params(self): -        ''' Returns a mix of GET and POST data. POST overwrites GET ''' +        """ Returns a mix of GET and POST data. POST overwrites GET """          if self._GETPOST is None:              self._GETPOST = dict(self.GET)              self._GETPOST.update(dict(self.POST)) @@ -334,7 +409,7 @@ class Request(threading.local):      @property      def COOKIES(self): -        """Returns a dict with COOKIES.""" +        """ Returns a dict with COOKIES. """          if self._COOKIES is None:              raw_dict = SimpleCookie(self._environ.get('HTTP_COOKIE',''))              self._COOKIES = {} @@ -357,6 +432,7 @@ class Response(threading.local):          self.header = HeaderWrapper(self.header_list)          self.content_type = 'text/html'          self.error = None +        self.charset = 'utf8'      def wsgiheaders(self):          ''' Returns a wsgi conform list of header/value pairs ''' @@ -371,19 +447,33 @@ class Response(threading.local):          return self._COOKIES      def set_cookie(self, key, value, **kargs): -        """ Sets a Cookie. Optional settings: expires, path, comment, domain, max-age, secure, version, httponly """ +        """ +        Sets a Cookie. Optional settings: +        expires, path, comment, domain, max-age, secure, version, httponly +        """          self.COOKIES[key] = value -        for k in kargs: -            self.COOKIES[key][k] = kargs[k] +        for k, v in kargs.iteritems(): +            self.COOKIES[key][k] = v      def get_content_type(self): -        '''Gives access to the 'Content-Type' header and defaults to 'text/html'.''' +        """ Get the current 'Content-Type' header. """          return self.header['Content-Type']      def set_content_type(self, value): +        if 'charset=' in value: +            self.charset = value.split('charset=')[-1].split(';')[0].strip()          self.header['Content-Type'] = value -         -    content_type = property(get_content_type, set_content_type, None, get_content_type.__doc__) + +    content_type = property(get_content_type, set_content_type, None, +                            get_content_type.__doc__) + + +class BaseController(object): +    _singleton = None +    def __new__(cls, *a, **k): +        if not cls._singleton: +            cls._singleton = object.__new__(cls, *a, **k) +        return cls._singleton  def abort(code=500, text='Unknown Error: Appliction stopped.'): @@ -398,12 +488,11 @@ def redirect(url, code=307):      raise BreakTheBottle("") -def send_file(filename, root, guessmime = True, mimetype = 'text/plain'): +def send_file(filename, root, guessmime = True, mimetype = None):      """ Aborts execution and sends a static files as response. """ -    root = os.path.abspath(root) + '/' -    filename = os.path.normpath(filename).strip('/') -    filename = os.path.join(root, filename) -     +    root = os.path.abspath(root) + os.sep +    filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) +      if not filename.startswith(root):          abort(401, "Access denied.")      if not os.path.exists(filename) or not os.path.isfile(filename): @@ -411,25 +500,41 @@ def send_file(filename, root, guessmime = True, mimetype = 'text/plain'):      if not os.access(filename, os.R_OK):          abort(401, "You do not have permission to access this file.") -    if guessmime: -        guess = mimetypes.guess_type(filename)[0] -        if guess: -            response.content_type = guess -        elif mimetype: -            response.content_type = mimetype -    elif mimetype: -        response.content_type = mimetype +    if guessmime and not mimetype: +        mimetype = mimetypes.guess_type(filename)[0] +    if not mimetype: mimetype = 'text/plain' +    response.content_type = mimetype      stats = os.stat(filename) -    # TODO: HTTP_IF_MODIFIED_SINCE -> 304 (Thu, 02 Jul 2009 23:16:31 CEST) -    if 'Content-Length' not in response.header: -        response.header['Content-Length'] = stats.st_size      if 'Last-Modified' not in response.header: -        ts = time.gmtime(stats.st_mtime) -        ts = time.strftime("%a, %d %b %Y %H:%M:%S +0000", ts) -        response.header['Last-Modified'] = ts +        lm = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(stats.st_mtime)) +        response.header['Last-Modified'] = lm +    if 'HTTP_IF_MODIFIED_SINCE' in request.environ: +        ims = request.environ['HTTP_IF_MODIFIED_SINCE'] +        # IE sends "<date>; length=146" +        ims = ims.split(";")[0].strip() +        ims = parse_date(ims) +        if ims is not None and ims >= stats.st_mtime: +           abort(304, "Not modified") +    if 'Content-Length' not in response.header: +        response.header['Content-Length'] = str(stats.st_size) +    raise BreakTheBottle(open(filename, 'rb')) + -    raise BreakTheBottle(open(filename, 'r')) +def parse_date(ims): +    """ +    Parses date strings usually found in HTTP header and returns UTC epoch. +    Understands rfc1123, rfc850 and asctime. +    """ +    try: +        ts = email.utils.parsedate_tz(ims) +        if ts is not None: +            if ts[9] is None: +                return time.mktime(ts[:8] + (0,)) - time.timezone +            else: +                return time.mktime(ts[:8] + (0,)) - ts[9] - time.timezone +    except (ValueError, IndexError): +        return None @@ -439,15 +544,17 @@ def send_file(filename, root, guessmime = True, mimetype = 'text/plain'):  # Decorators  def validate(**vkargs): -    ''' Validates and manipulates keyword arguments by user defined callables  -    and handles ValueError and missing arguments by raising HTTPError(403) ''' +    """ +    Validates and manipulates keyword arguments by user defined callables.  +    Handles ValueError and missing arguments by raising HTTPError(403). +    """      def decorator(func):          def wrapper(**kargs): -            for key in vkargs: +            for key, value in vkargs.iteritems():                  if key not in kargs:                      abort(403, 'Missing parameter: %s' % key)                  try: -                    kargs[key] = vkargs[key](kargs[key]) +                    kargs[key] = value(kargs[key])                  except ValueError, e:                      abort(403, 'Wrong parameter format for: %s' % key)              return func(**kargs) @@ -456,12 +563,21 @@ def validate(**vkargs):  def route(url, **kargs): -    """ Decorator for request handler. Same as add_route(url, handler, **kargs).""" +    """ +    Decorator for request handler. Same as add_route(url, handler, **kargs). +    """      return default_app().route(url, **kargs) +def default(): +    """ +    Decorator for request handler. Same as set_default(handler). +    """ +    return default_app().default()  def error(code=500): -    """ Decorator for error handler. Same as set_error_handler(code, handler).""" +    """ +    Decorator for error handler. Same as set_error_handler(code, handler). +    """      return default_app().error(code) @@ -471,8 +587,23 @@ def error(code=500):  # Server adapter -class ServerAdapter(object): +class WSGIAdapter(object): +    def run(self, handler): # pragma: no cover +        pass + +    def __repr__(self): +        return "%s()" % (self.__class__.__name__) + + +class CGIServer(WSGIAdapter): +    def run(self, handler): +        from wsgiref.handlers import CGIHandler +        CGIHandler().run(handler) + + +class ServerAdapter(WSGIAdapter):      def __init__(self, host='127.0.0.1', port=8080, **kargs): +        WSGIAdapter.__init__(self)          self.host = host          self.port = int(port)          self.options = kargs @@ -480,9 +611,6 @@ class ServerAdapter(object):      def __repr__(self):          return "%s (%s:%d)" % (self.__class__.__name__, self.host, self.port) -    def run(self, handler): -        pass -  class WSGIRefServer(ServerAdapter):      def run(self, handler): @@ -513,12 +641,14 @@ class PasteServer(ServerAdapter):  class FapwsServer(ServerAdapter): -    """ Extreamly fast Webserver using libev (see http://william-os4y.livejournal.com/) -        Experimental ... """ +    """ +    Extremly fast webserver using libev. +    See http://william-os4y.livejournal.com/ +    Experimental ... +    """      def run(self, handler):          import fapws._evwsgi as evwsgi          from fapws import base -        import sys          evwsgi.start(self.host, self.port)          evwsgi.set_base_module(base)          def app(environ, start_response): @@ -528,35 +658,75 @@ class FapwsServer(ServerAdapter):          evwsgi.run() -def run(app=None, server=WSGIRefServer, host='127.0.0.1', port=8080, **kargs): -    """ Runs bottle as a web server, using Python's built-in wsgiref implementation by default. -     -    You may choose between WSGIRefServer, CherryPyServer, FlupServer and -    PasteServer or write your own server adapter. -    """ +def run(app=None, server=WSGIRefServer, host='127.0.0.1', port=8080, +        interval=1, reloader=False, **kargs): +    """ Runs bottle as a web server. """      if not app:          app = default_app() -    quiet = bool('quiet' in kargs and kargs['quiet']) - -    # Instanciate server, if it is a class instead of an instance -    if isinstance(server, type) and issubclass(server, ServerAdapter): -        server = server(host=host, port=port, **kargs) +    quiet = bool(kargs.get('quiet', False)) -    if not isinstance(server, ServerAdapter): -        raise RuntimeError("Server must be a subclass of ServerAdapter") +    # Instantiate server, if it is a class instead of an instance +    if isinstance(server, type): +        if issubclass(server, CGIServer): +            server = server() +        elif issubclass(server, ServerAdapter): +            server = server(host=host, port=port, **kargs) -    if not quiet: -        print 'Bottle server starting up (using %s)...' % repr(server) -        print 'Listening on http://%s:%d/' % (server.host, server.port) -        print 'Use Ctrl-C to quit.' -        print +    if not isinstance(server, WSGIAdapter): +        raise RuntimeError("Server must be a subclass of WSGIAdapter") +  +    if not quiet and isinstance(server, ServerAdapter): # pragma: no cover +        if not reloader or os.environ.get('BOTTLE_CHILD') == 'true': +            print "Bottle server starting up (using %s)..." % repr(server) +            print "Listening on http://%s:%d/" % (server.host, server.port) +            print "Use Ctrl-C to quit." +            print +        else: +            print "Bottle auto reloader starting up..."      try: -        server.run(app) +        if reloader and interval: +            reloader_run(server, app, interval) +        else: +            server.run(app)      except KeyboardInterrupt: -        print "Shuting down..." - +        if not quiet: # pragma: no cover +            print "Shutting Down..." + + +#TODO: If the parent process is killed (with SIGTERM) the childs survive... +def reloader_run(server, app, interval): +    if os.environ.get('BOTTLE_CHILD') == 'true': +        # We are a child process +        files = dict() +        for module in sys.modules.values(): +            file_path = getattr(module, '__file__', None) +            if file_path and os.path.isfile(file_path): +                file_split = os.path.splitext(file_path) +                if file_split[1] in ('.py', '.pyc', '.pyo'): +                    file_path = file_split[0] + '.py' +                    files[file_path] = os.stat(file_path).st_mtime +        thread.start_new_thread(server.run, (app,)) +        while True: +            time.sleep(interval) +            for file_path, file_mtime in files.iteritems(): +                if not os.path.exists(file_path): +                    print "File changed: %s (deleted)" % file_path +                elif os.stat(file_path).st_mtime > file_mtime: +                    print "File changed: %s (modified)" % file_path +                else: continue +                print "Restarting..." +                app.serve = False +                time.sleep(interval) # be nice and wait for running requests +                sys.exit(3) +    while True: +        args = [sys.executable] + sys.argv +        environ = os.environ.copy() +        environ['BOTTLE_CHILD'] = 'true' +        exit_status = subprocess.call(args, env=environ) +        if exit_status != 3: +            sys.exit(exit_status) @@ -564,89 +734,183 @@ def run(app=None, server=WSGIRefServer, host='127.0.0.1', port=8080, **kargs):  # Templates +class TemplateError(HTTPError): +    def __init__(self, message): +        HTTPError.__init__(self, 500, message) +  class BaseTemplate(object): -    def __init__(self, template='', filename=None): -        self.source = filename -        if self.source: -            fp = open(filename) -            template = fp.read() -            fp.close() -        self.parse(template) - -    def parse(self, template): raise NotImplementedError -    def render(self, **args): raise NotImplementedError -     -    @classmethod -    def find(cls, name): -        for path in TEMPLATE_PATH: -            if os.path.isfile(path % name): -                return cls(filename = path % name) -        return None +    def __init__(self, template='', name=None, filename=None, lookup=[]): +        """ +        Create a new template. +        If a name is provided, but no filename and no template string, the +        filename is guessed using the lookup path list. +        Subclasses can assume that either self.template or self.filename is set. +        If both are present, self.template should be used. +        """ +        self.name = name +        self.filename = filename +        self.template = template +        self.lookup = lookup +        if self.name and not self.filename: +            for path in self.lookup: +                fpath = os.path.join(path, self.name+'.tpl') +                if os.path.isfile(fpath): +                    self.filename = fpath +        if not self.template and not self.filename: +            raise TemplateError('Template (%s) not found.' % self.name) +        self.prepare() + +    def prepare(self): +        """ +        Run preparatios (parsing, caching, ...). +        It should be possible to call this multible times to refresh a template. +        """ +        raise NotImplementedError + +    def render(self, **args): +        """ +        Render the template with the specified local variables and return an +        iterator of strings (bytes). This must be thread save! +        """ +        raise NotImplementedError  class MakoTemplate(BaseTemplate): -    def parse(self, template): +    output_encoding=None +    input_encoding=None +    default_filters=None +    global_variables={} + +    def prepare(self):          from mako.template import Template -        self.tpl = Template(template) +        from mako.lookup import TemplateLookup +        #TODO: This is a hack... http://github.com/defnull/bottle/issues#issue/8 +        mylookup = TemplateLookup(directories=map(os.path.abspath, self.lookup)+['./']) +        if self.template: +            self.tpl = Template(self.template, +                                lookup=mylookup, +                                output_encoding=MakoTemplate.output_encoding, +                                input_encoding=MakoTemplate.input_encoding, +                                default_filters=MakoTemplate.default_filters +                                ) +        else: +            self.tpl = Template(filename=self.filename, +                                lookup=mylookup, +                                output_encoding=MakoTemplate.output_encoding, +                                input_encoding=MakoTemplate.input_encoding, +                                default_filters=MakoTemplate.default_filters +                                )      def render(self, **args): -        return self.tpl.render(**args) +        _defaults = MakoTemplate.global_variables.copy() +        _defaults.update(args) +        return self.tpl.render(**_defaults)  class CheetahTemplate(BaseTemplate): -    def parse(self, template): +    def prepare(self):          from Cheetah.Template import Template          self.context = threading.local()          self.context.vars = {} -        self.tpl = Template(source = template, searchList=[self.context.vars]) +        if self.template: +            self.tpl = Template(source=self.template, searchList=[self.context.vars]) +        else: +            self.tpl = Template(file=self.filename, searchList=[self.context.vars])      def render(self, **args):          self.context.vars.update(args)          out = str(self.tpl)          self.context.vars.clear() -        return out +        return [out] + + +class Jinja2Template(BaseTemplate): +    env = None # hopefully, a Jinja environment is actually thread-safe + +    def prepare(self): +        if not self.env: +            from jinja2 import Environment, FunctionLoader +            self.env = Environment(line_statement_prefix="#", loader=FunctionLoader(self.loader)) +        if self.template: +            self.tpl = self.env.from_string(self.template) +        else: +            self.tpl = self.env.get_template(self.filename) + +    def render(self, **args): +        return self.tpl.render(**args).encode("utf-8") +         +    def loader(self, name): +        if not name.endswith(".tpl"): +            for path in self.lookup: +                fpath = os.path.join(path, name+'.tpl') +                if os.path.isfile(fpath): +                    name = fpath +                    break +        f = open(name) +        try: return f.read() +        finally: f.close()  class SimpleTemplate(BaseTemplate): -    re_python = re.compile(r'^\s*%\s*(?:(if|elif|else|try|except|finally|for|while|with|def|class)|(include)|(end)|(.*))') +    re_python = re.compile(r'^\s*%\s*(?:(if|elif|else|try|except|finally|for|' +                            'while|with|def|class)|(include|rebase)|(end)|(.*))')      re_inline = re.compile(r'\{\{(.*?)\}\}')      dedent_keywords = ('elif', 'else', 'except', 'finally') +    def prepare(self): +        if self.template: +            code = self.translate(self.template) +            self.co = compile(code, '<string>', 'exec') +        else: +            code = self.translate(open(self.filename).read()) +            self.co = compile(code, self.filename, 'exec') +      def translate(self, template):          indent = 0          strbuffer = []          code = [] -        self.subtemplates = {} +        self.includes = dict()          class PyStmt(str):              def __repr__(self): return 'str(' + self + ')'          def flush(allow_nobreak=False):              if len(strbuffer):                  if allow_nobreak and strbuffer[-1].endswith("\\\\\n"):                      strbuffer[-1]=strbuffer[-1][:-3] -                code.append(" " * indent + "stdout.append(%s)" % repr(''.join(strbuffer))) -                code.append((" " * indent + "\n") * len(strbuffer)) # to preserve line numbers  +                code.append(' ' * indent + "_stdout.append(%s)" % repr(''.join(strbuffer))) +                code.append((' ' * indent + '\n') * len(strbuffer)) # to preserve line numbers                   del strbuffer[:]          for line in template.splitlines(True):              m = self.re_python.match(line)              if m:                  flush(allow_nobreak=True) -                keyword, include, end, statement = m.groups() +                keyword, subtpl, end, statement = m.groups()                  if keyword:                      if keyword in self.dedent_keywords:                          indent -= 1                      code.append(" " * indent + line[m.start(1):])                      indent += 1 -                elif include: +                elif subtpl:                      tmp = line[m.end(2):].strip().split(None, 1) -                    name = tmp[0] -                    args = tmp[1:] and tmp[1] or '' -                    self.subtemplates[name] = SimpleTemplate.find(name) -                    code.append(" " * indent + "stdout.append(_subtemplates[%s].render(%s))\n" % (repr(name), args)) +                    if not tmp: +                      code.append(' ' * indent + "_stdout.extend(_base)\n") +                    else: +                      name = tmp[0] +                      args = tmp[1:] and tmp[1] or '' +                      if name not in self.includes: +                        self.includes[name] = SimpleTemplate(name=name, lookup=self.lookup) +                      if subtpl == 'include': +                        code.append(' ' * indent +  +                                    "_ = _includes[%s].execute(_stdout, %s)\n" +                                    % (repr(name), args)) +                      else: +                        code.append(' ' * indent +  +                                    "_tpl['_rebase'] = (_includes[%s], dict(%s))\n" +                                    % (repr(name), args))                  elif end:                      indent -= 1 -                    code.append(" " * indent + '#' + line[m.start(3):]) +                    code.append(' ' * indent + '#' + line[m.start(3):])                  elif statement: -                    code.append(" " * indent + line[m.start(4):]) +                    code.append(' ' * indent + line[m.start(4):])              else:                  splits = self.re_inline.split(line) # text, (expr, text)*                  if len(splits) == 1: @@ -656,40 +920,86 @@ class SimpleTemplate(BaseTemplate):                      for i in range(1, len(splits), 2):                          splits[i] = PyStmt(splits[i])                      splits = [x for x in splits if bool(x)] -                    code.append(" " * indent + "stdout.extend(%s)\n" % repr(splits)) +                    code.append(' ' * indent + "_stdout.extend(%s)\n" % repr(splits))          flush()          return ''.join(code) -    def parse(self, template): -        code = self.translate(template) -        self.co = compile(code, self.source or '<template>', 'exec') - -    def render(self, **args): -        ''' Returns the rendered template using keyword arguments as local variables. ''' -        args['stdout'] = [] -        args['_subtemplates'] = self.subtemplates +    def execute(self, stdout, **args): +        args['_stdout'] = stdout +        args['_includes'] = self.includes +        args['_tpl'] = args          eval(self.co, args) -        return ''.join(args['stdout']) +        if '_rebase' in args: +            subtpl, args = args['_rebase'] +            args['_base'] = stdout[:] #copy stdout +            del stdout[:] # clear stdout +            return subtpl.execute(stdout, **args) +        return args +    def render(self, **args): +        """ Render the template using keyword arguments as local variables. """ +        stdout = [] +        self.execute(stdout, **args) +        return stdout +             -def template(template, template_adapter=SimpleTemplate, **args): -    ''' Returns a string from a template ''' -    if template not in TEMPLATES: -        if template.find("\n") == template.find("{") == template.find("%") == -1: -            TEMPLATES[template] = template_adapter.find(template) +def template(tpl, template_adapter=SimpleTemplate, **args): +    ''' +    Get a rendered template as a string iterator. +    You can use a name, a filename or a template string as first parameter. +    ''' +    lookup = args.get('template_lookup', TEMPLATE_PATH) +    if tpl not in TEMPLATES or DEBUG: +        if "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl: +            TEMPLATES[tpl] = template_adapter(template=tpl, lookup=lookup) +        elif '.' in tpl: +            TEMPLATES[tpl] = template_adapter(filename=tpl, lookup=lookup)          else: -            TEMPLATES[template] = template_adapter(template) -    if not TEMPLATES[template]: -        abort(500, 'Template not found') +            TEMPLATES[tpl] = template_adapter(name=tpl, lookup=lookup) +    if not TEMPLATES[tpl]: +        abort(500, 'Template (%s) not found' % tpl)      args['abort'] = abort      args['request'] = request      args['response'] = response -    return TEMPLATES[template].render(**args) +    return TEMPLATES[tpl].render(**args) + + +def mako_template(tpl_name, **kargs): +    kargs['template_adapter'] = MakoTemplate +    return template(tpl_name, **kargs) + +def cheetah_template(tpl_name, **kargs): +    kargs['template_adapter'] = CheetahTemplate +    return template(tpl_name, **kargs) + +def jinja2_template(tpl_name, **kargs): +    kargs['template_adapter'] = Jinja2Template +    return template(tpl_name, **kargs) +def view(tpl_name, **defaults): +    ''' Decorator: Rendes a template for a handler. +        Return a dict of template vars to fill out the template. +    ''' +    def decorator(func): +        def wrapper(**kargs): +            out = func(**kargs) +            defaults.update(out) +            return template(tpl_name, **defaults) +        return wrapper +    return decorator + +def mako_view(tpl_name, **kargs): +    kargs['template_adapter'] = MakoTemplate +    return view(tpl_name, **kargs) + +def cheetah_view(tpl_name, **kargs): +    kargs['template_adapter'] = CheetahTemplate +    return view(tpl_name, **kargs) -def mako_template(template_name, **args): return template(template_name, template_adapter=MakoTemplate, **args) +def jinja2_view(tpl_name, **kargs): +    kargs['template_adapter'] = Jinja2Template +    return view(tpl_name, **kargs) -def cheetah_template(template_name, **args): return template(template_name, template_adapter=CheetahTemplate, **args) @@ -698,8 +1008,8 @@ def cheetah_template(template_name, **args): return template(template_name, temp  # Database -class BottleBucket(object): -    '''Memory-caching wrapper around anydbm''' +class BottleBucket(object): # pragma: no cover +    """ Memory-caching wrapper around anydbm """      def __init__(self, name):          self.__dict__['name'] = name          self.__dict__['db'] = dbm.open(DB_PATH + '/%s.db' % name, 'c') @@ -711,6 +1021,7 @@ class BottleBucket(object):          return self.mmap[key]      def __setitem__(self, key, value): +        if not isinstance(key, str): raise TypeError("Bottle keys must be strings")          self.mmap[key] = value      def __delitem__(self, key): @@ -754,7 +1065,10 @@ class BottleBucket(object):              if key not in self.db or pvalue != self.db[key]:                  self.db[key] = pvalue          self.mmap.clear() -        self.db.close() +        if hasattr(self.db, 'sync'): +            self.db.sync() +        if hasattr(self.db, 'close'): +            self.db.close()      def clear(self):          for key in self.db: @@ -773,12 +1087,13 @@ class BottleBucket(object):              raise -class BottleDB(threading.local): -    '''Holds multible BottleBucket instances in a thread-local way.''' +class BottleDB(threading.local): # pragma: no cover +    """ Holds multible BottleBucket instances in a thread-local way. """      def __init__(self):          self.__dict__['open'] = {}      def __getitem__(self, key): +        warnings.warn("Please do not use bottle.db anymore. This feature is deprecated. You may use anydb directly.", DeprecationWarning)          if key not in self.open and not key.startswith('_'):              self.open[key] = BottleBucket(key)          return self.open[key] @@ -829,8 +1144,9 @@ class BottleDB(threading.local):  # Modul initialization and configuration  DB_PATH = './' -TEMPLATE_PATH = ['./%s.tpl', './views/%s.tpl'] +TEMPLATE_PATH = ['./', './views/']  TEMPLATES = {} +DEBUG = False  HTTP_CODES = {      100: 'CONTINUE',      101: 'SWITCHING PROTOCOLS', @@ -875,12 +1191,41 @@ HTTP_CODES = {      505: 'HTTP VERSION NOT SUPPORTED',  } +HTTP_ERROR_TEMPLATE = """ +<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> +<html> +    <head> +        <title>Error %(status)d: %(error_name)s</title> +    </head> +    <body> +        <h1>Error %(status)d: %(error_name)s</h1> +        <p>Sorry, the requested URL <tt>%(url)s</tt> caused an error:</p> +        <pre> +            %(error_message)s +        </pre> +    </body> +</html> +""" + +TRACEBACK_TEMPLATE = """ +<h2>Traceback:</h2> +<pre> +%s +</pre> +""" +  request = Request()  response = Response()  db = BottleDB()  local = threading.local() -def debug(mode=True): default_app().debug = bool(mode) -def optimize(mode=True): default_app().optimize = bool(mode) +#TODO: Global and app local configuration (debug, defaults, ...) is a mess + +def debug(mode=True): +    global DEBUG +    DEBUG = bool(mode) + +def optimize(mode=True): +    default_app().optimize = bool(mode) diff --git a/pyLoadCore.py b/pyLoadCore.py index b78cf5b5c..fc050a309 100755 --- a/pyLoadCore.py +++ b/pyLoadCore.py @@ -46,10 +46,15 @@ from time import sleep  import urllib2  from imp import find_module  from re import sub +try:  +    find_module("Crypto") +except ImportError: +    print "Install pycrypto to use pyLoad" +    exit()  from module.file_list import File_List  from module.thread_list import Thread_List  from module.network.Request import Request -import module.remote.SecureXMLRPCServer as Server +from module.web import WebServer  import thread  class Core(object): @@ -112,7 +117,6 @@ class Core(object):          translation = gettext.translation("pyLoad", "locale", languages=[self.config['general']['language']])          translation.install(unicode=True) -        self.check_install("Crypto", "pycrypto to decode container files")          self.check_install("pycurl", "pycurl for lower memory footprint while downloading")          self.check_install("tesseract", "tesseract for captcha reading", False)          self.check_install("gocr", "gocr for captcha reading", False) @@ -140,6 +144,8 @@ class Core(object):          self.init_server() +        self.init_webserver() +                  self.logger.info(_("Downloadtime: %s") % self.server_methods.is_time_download()) # debug only          self.read_url_list(self.config['general']['link_file']) @@ -174,6 +180,7 @@ class Core(object):          try:              server_addr = (self.config['remote']['listenaddr'], int(self.config['remote']['port']))              usermap = { self.config['remote']['username']: self.config['remote']['password']} +            Server = __import__("module.remote.SecureXMLRPCServer", globals(), locals(), "SecureXMLRPCServer", -1)              if self.config['ssl']['activated']:                  self.server = Server.SecureXMLRPCServer(server_addr, self.config['ssl']['cert'], self.config['ssl']['key'], usermap)                  self.logger.info("Secure XMLRPC Server Started") @@ -240,31 +247,19 @@ class Core(object):      def check_update(self):          """checks newst version""" -        if self.config['updates']['search_updates']: -            version_check = Request().load("http://update.pyload.org/index.php?do=dev%s&download=%s" %(CURRENT_VERSION, self.config['updates']['install_updates'])) -            if version_check == "": -                self.logger.info("No Updates for pyLoad") -                return False +        if not self.config['updates']['search_updates']: +            return False +         +        newst_version = Request().load("http://update.pyload.org/s/" + CURRENT_VERSION) +        if newst_version == "True": +            if not self.config['updates']['install_updates']: +                self.logger.info("New Version of pyLoad available")              else: -                if self.config['updates']['install_updates']: -                    try: -                        tmp_zip_name = __import__("tempfile").NamedTemporaryFile(suffix=".zip").name -                        tmp_zip = open(tmp_zip_name, 'w') -                        tmp_zip.write(version_check) -                        tmp_zip.close() -                        __import__("module.Unzip", globals(), locals(), "Unzip", -1).Unzip().extract(tmp_zip_name,"Test/") -                        return True - -                    except: -                        self.logger.info("Auto install Faild") -                        return False - -                else: -                    self.logger.info("New pyLoad Version %s available" % version_check) -                    return True +                updater = __import__("pyLoadUpdater") +                updater.main()          else: -            return False -             +            self.logger.info("No Updates for pyLoad") +      def create_plugin_index(self):          for file_handler in glob(self.plugin_folder + sep + '*.py') + glob(self.plugin_folder + sep + 'DLC.pyc'):              plugin_pattern = "" @@ -289,7 +284,15 @@ class Core(object):          elif start > end and (now > start or now < end): return True          elif start < now and end < now and start > end: return True          else: return False -         +    +    +    def init_webserver(self): +        self.webserver = WebServer.WebServer(self) +        if self.config['webinterface']['activated']: +            self.webserver.start() +             + +      ####################################      ########## XMLRPC Methods ##########      #################################### @@ -436,6 +439,8 @@ class ServerMethods():          end = self.core.config['reconnectTime']['end'].split(":")          return self.compare_time(start, end) + +  if __name__ == "__main__":      pyload_core = Core()      try: | 
