Home | History | Annotate | Download | only in paste
      1 # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
      2 # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
      3 # (c) 2005 Clark C. Evans
      4 # This module is part of the Python Paste Project and is released under
      5 # the MIT License: http://www.opensource.org/licenses/mit-license.php
      6 # This code was written with funding by http://prometheusresearch.com
      7 """
      8 Upload Progress Monitor
      9 
     10 This is a WSGI middleware component which monitors the status of files
     11 being uploaded.  It includes a small query application which will return
     12 a list of all files being uploaded by particular session/user.
     13 
     14 >>> from paste.httpserver import serve
     15 >>> from paste.urlmap import URLMap
     16 >>> from paste.auth.basic import AuthBasicHandler
     17 >>> from paste.debug.debugapp import SlowConsumer, SimpleApplication
     18 >>> # from paste.progress import *
     19 >>> realm = 'Test Realm'
     20 >>> def authfunc(username, password):
     21 ...     return username == password
     22 >>> map = URLMap({})
     23 >>> ups = UploadProgressMonitor(map, threshold=1024)
     24 >>> map['/upload'] = SlowConsumer()
     25 >>> map['/simple'] = SimpleApplication()
     26 >>> map['/report'] = UploadProgressReporter(ups)
     27 >>> serve(AuthBasicHandler(ups, realm, authfunc))
     28 serving on...
     29 
     30 .. note::
     31 
     32    This is experimental, and will change in the future.
     33 """
     34 import time
     35 from paste.wsgilib import catch_errors
     36 
     37 DEFAULT_THRESHOLD = 1024 * 1024  # one megabyte
     38 DEFAULT_TIMEOUT   = 60*5         # five minutes
     39 ENVIRON_RECEIVED  = 'paste.bytes_received'
     40 REQUEST_STARTED   = 'paste.request_started'
     41 REQUEST_FINISHED  = 'paste.request_finished'
     42 
     43 class _ProgressFile(object):
     44     """
     45     This is the input-file wrapper used to record the number of
     46     ``paste.bytes_received`` for the given request.
     47     """
     48 
     49     def __init__(self, environ, rfile):
     50         self._ProgressFile_environ = environ
     51         self._ProgressFile_rfile   = rfile
     52         self.flush = rfile.flush
     53         self.write = rfile.write
     54         self.writelines = rfile.writelines
     55 
     56     def __iter__(self):
     57         environ = self._ProgressFile_environ
     58         riter = iter(self._ProgressFile_rfile)
     59         def iterwrap():
     60             for chunk in riter:
     61                 environ[ENVIRON_RECEIVED] += len(chunk)
     62                 yield chunk
     63         return iter(iterwrap)
     64 
     65     def read(self, size=-1):
     66         chunk = self._ProgressFile_rfile.read(size)
     67         self._ProgressFile_environ[ENVIRON_RECEIVED] += len(chunk)
     68         return chunk
     69 
     70     def readline(self):
     71         chunk = self._ProgressFile_rfile.readline()
     72         self._ProgressFile_environ[ENVIRON_RECEIVED] += len(chunk)
     73         return chunk
     74 
     75     def readlines(self, hint=None):
     76         chunk = self._ProgressFile_rfile.readlines(hint)
     77         self._ProgressFile_environ[ENVIRON_RECEIVED] += len(chunk)
     78         return chunk
     79 
     80 class UploadProgressMonitor(object):
     81     """
     82     monitors and reports on the status of uploads in progress
     83 
     84     Parameters:
     85 
     86         ``application``
     87 
     88             This is the next application in the WSGI stack.
     89 
     90         ``threshold``
     91 
     92             This is the size in bytes that is needed for the
     93             upload to be included in the monitor.
     94 
     95         ``timeout``
     96 
     97             This is the amount of time (in seconds) that a upload
     98             remains in the monitor after it has finished.
     99 
    100     Methods:
    101 
    102         ``uploads()``
    103 
    104             This returns a list of ``environ`` dict objects for each
    105             upload being currently monitored, or finished but whose time
    106             has not yet expired.
    107 
    108     For each request ``environ`` that is monitored, there are several
    109     variables that are stored:
    110 
    111         ``paste.bytes_received``
    112 
    113             This is the total number of bytes received for the given
    114             request; it can be compared with ``CONTENT_LENGTH`` to
    115             build a percentage complete.  This is an integer value.
    116 
    117         ``paste.request_started``
    118 
    119             This is the time (in seconds) when the request was started
    120             as obtained from ``time.time()``.  One would want to format
    121             this for presentation to the user, if necessary.
    122 
    123         ``paste.request_finished``
    124 
    125             This is the time (in seconds) when the request was finished,
    126             canceled, or otherwise disconnected.  This is None while
    127             the given upload is still in-progress.
    128 
    129     TODO: turn monitor into a queue and purge queue of finished
    130           requests that have passed the timeout period.
    131     """
    132     def __init__(self, application, threshold=None, timeout=None):
    133         self.application = application
    134         self.threshold = threshold or DEFAULT_THRESHOLD
    135         self.timeout   = timeout   or DEFAULT_TIMEOUT
    136         self.monitor   = []
    137 
    138     def __call__(self, environ, start_response):
    139         length = environ.get('CONTENT_LENGTH', 0)
    140         if length and int(length) > self.threshold:
    141             # replace input file object
    142             self.monitor.append(environ)
    143             environ[ENVIRON_RECEIVED] = 0
    144             environ[REQUEST_STARTED] = time.time()
    145             environ[REQUEST_FINISHED] = None
    146             environ['wsgi.input'] = \
    147                 _ProgressFile(environ, environ['wsgi.input'])
    148             def finalizer(exc_info=None):
    149                 environ[REQUEST_FINISHED] = time.time()
    150             return catch_errors(self.application, environ,
    151                        start_response, finalizer, finalizer)
    152         return self.application(environ, start_response)
    153 
    154     def uploads(self):
    155         return self.monitor
    156 
    157 class UploadProgressReporter(object):
    158     """
    159     reports on the progress of uploads for a given user
    160 
    161     This reporter returns a JSON file (for use in AJAX) listing the
    162     uploads in progress for the given user.  By default, this reporter
    163     uses the ``REMOTE_USER`` environment to compare between the current
    164     request and uploads in-progress.  If they match, then a response
    165     record is formed.
    166 
    167         ``match()``
    168 
    169             This member function can be overriden to provide alternative
    170             matching criteria.  It takes two environments, the first
    171             is the current request, the second is a current upload.
    172 
    173         ``report()``
    174 
    175             This member function takes an environment and builds a
    176             ``dict`` that will be used to create a JSON mapping for
    177             the given upload.  By default, this just includes the
    178             percent complete and the request url.
    179 
    180     """
    181     def __init__(self, monitor):
    182         self.monitor   = monitor
    183 
    184     def match(self, search_environ, upload_environ):
    185         if search_environ.get('REMOTE_USER', None) == \
    186            upload_environ.get('REMOTE_USER', 0):
    187             return True
    188         return False
    189 
    190     def report(self, environ):
    191         retval = { 'started': time.strftime("%Y-%m-%d %H:%M:%S",
    192                                 time.gmtime(environ[REQUEST_STARTED])),
    193                    'finished': '',
    194                    'content_length': environ.get('CONTENT_LENGTH'),
    195                    'bytes_received': environ[ENVIRON_RECEIVED],
    196                    'path_info': environ.get('PATH_INFO',''),
    197                    'query_string': environ.get('QUERY_STRING','')}
    198         finished = environ[REQUEST_FINISHED]
    199         if finished:
    200             retval['finished'] = time.strftime("%Y:%m:%d %H:%M:%S",
    201                                                time.gmtime(finished))
    202         return retval
    203 
    204     def __call__(self, environ, start_response):
    205         body = []
    206         for map in [self.report(env) for env in self.monitor.uploads()
    207                                              if self.match(environ, env)]:
    208             parts = []
    209             for k, v in map.items():
    210                 v = str(v).replace("\\", "\\\\").replace('"', '\\"')
    211                 parts.append('%s: "%s"' % (k, v))
    212             body.append("{ %s }" % ", ".join(parts))
    213         body = "[ %s ]" % ", ".join(body)
    214         start_response("200 OK", [('Content-Type', 'text/plain'),
    215                                   ('Content-Length', len(body))])
    216         return [body]
    217 
    218 __all__ = ['UploadProgressMonitor', 'UploadProgressReporter']
    219 
    220 if "__main__" == __name__:
    221     import doctest
    222     doctest.testmod(optionflags=doctest.ELLIPSIS)
    223