| # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) |
| # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php |
| # (c) 2005 Clark C. Evans |
| # This module is part of the Python Paste Project and is released under |
| # the MIT License: http://www.opensource.org/licenses/mit-license.php |
| # This code was written with funding by http://prometheusresearch.com |
| """ |
| Upload Progress Monitor |
| |
| This is a WSGI middleware component which monitors the status of files |
| being uploaded. It includes a small query application which will return |
| a list of all files being uploaded by particular session/user. |
| |
| >>> from paste.httpserver import serve |
| >>> from paste.urlmap import URLMap |
| >>> from paste.auth.basic import AuthBasicHandler |
| >>> from paste.debug.debugapp import SlowConsumer, SimpleApplication |
| >>> # from paste.progress import * |
| >>> realm = 'Test Realm' |
| >>> def authfunc(username, password): |
| ... return username == password |
| >>> map = URLMap({}) |
| >>> ups = UploadProgressMonitor(map, threshold=1024) |
| >>> map['/upload'] = SlowConsumer() |
| >>> map['/simple'] = SimpleApplication() |
| >>> map['/report'] = UploadProgressReporter(ups) |
| >>> serve(AuthBasicHandler(ups, realm, authfunc)) |
| serving on... |
| |
| .. note:: |
| |
| This is experimental, and will change in the future. |
| """ |
| import time |
| from paste.wsgilib import catch_errors |
| |
| DEFAULT_THRESHOLD = 1024 * 1024 # one megabyte |
| DEFAULT_TIMEOUT = 60*5 # five minutes |
| ENVIRON_RECEIVED = 'paste.bytes_received' |
| REQUEST_STARTED = 'paste.request_started' |
| REQUEST_FINISHED = 'paste.request_finished' |
| |
| class _ProgressFile(object): |
| """ |
| This is the input-file wrapper used to record the number of |
| ``paste.bytes_received`` for the given request. |
| """ |
| |
| def __init__(self, environ, rfile): |
| self._ProgressFile_environ = environ |
| self._ProgressFile_rfile = rfile |
| self.flush = rfile.flush |
| self.write = rfile.write |
| self.writelines = rfile.writelines |
| |
| def __iter__(self): |
| environ = self._ProgressFile_environ |
| riter = iter(self._ProgressFile_rfile) |
| def iterwrap(): |
| for chunk in riter: |
| environ[ENVIRON_RECEIVED] += len(chunk) |
| yield chunk |
| return iter(iterwrap) |
| |
| def read(self, size=-1): |
| chunk = self._ProgressFile_rfile.read(size) |
| self._ProgressFile_environ[ENVIRON_RECEIVED] += len(chunk) |
| return chunk |
| |
| def readline(self): |
| chunk = self._ProgressFile_rfile.readline() |
| self._ProgressFile_environ[ENVIRON_RECEIVED] += len(chunk) |
| return chunk |
| |
| def readlines(self, hint=None): |
| chunk = self._ProgressFile_rfile.readlines(hint) |
| self._ProgressFile_environ[ENVIRON_RECEIVED] += len(chunk) |
| return chunk |
| |
| class UploadProgressMonitor(object): |
| """ |
| monitors and reports on the status of uploads in progress |
| |
| Parameters: |
| |
| ``application`` |
| |
| This is the next application in the WSGI stack. |
| |
| ``threshold`` |
| |
| This is the size in bytes that is needed for the |
| upload to be included in the monitor. |
| |
| ``timeout`` |
| |
| This is the amount of time (in seconds) that a upload |
| remains in the monitor after it has finished. |
| |
| Methods: |
| |
| ``uploads()`` |
| |
| This returns a list of ``environ`` dict objects for each |
| upload being currently monitored, or finished but whose time |
| has not yet expired. |
| |
| For each request ``environ`` that is monitored, there are several |
| variables that are stored: |
| |
| ``paste.bytes_received`` |
| |
| This is the total number of bytes received for the given |
| request; it can be compared with ``CONTENT_LENGTH`` to |
| build a percentage complete. This is an integer value. |
| |
| ``paste.request_started`` |
| |
| This is the time (in seconds) when the request was started |
| as obtained from ``time.time()``. One would want to format |
| this for presentation to the user, if necessary. |
| |
| ``paste.request_finished`` |
| |
| This is the time (in seconds) when the request was finished, |
| canceled, or otherwise disconnected. This is None while |
| the given upload is still in-progress. |
| |
| TODO: turn monitor into a queue and purge queue of finished |
| requests that have passed the timeout period. |
| """ |
| def __init__(self, application, threshold=None, timeout=None): |
| self.application = application |
| self.threshold = threshold or DEFAULT_THRESHOLD |
| self.timeout = timeout or DEFAULT_TIMEOUT |
| self.monitor = [] |
| |
| def __call__(self, environ, start_response): |
| length = environ.get('CONTENT_LENGTH', 0) |
| if length and int(length) > self.threshold: |
| # replace input file object |
| self.monitor.append(environ) |
| environ[ENVIRON_RECEIVED] = 0 |
| environ[REQUEST_STARTED] = time.time() |
| environ[REQUEST_FINISHED] = None |
| environ['wsgi.input'] = \ |
| _ProgressFile(environ, environ['wsgi.input']) |
| def finalizer(exc_info=None): |
| environ[REQUEST_FINISHED] = time.time() |
| return catch_errors(self.application, environ, |
| start_response, finalizer, finalizer) |
| return self.application(environ, start_response) |
| |
| def uploads(self): |
| return self.monitor |
| |
| class UploadProgressReporter(object): |
| """ |
| reports on the progress of uploads for a given user |
| |
| This reporter returns a JSON file (for use in AJAX) listing the |
| uploads in progress for the given user. By default, this reporter |
| uses the ``REMOTE_USER`` environment to compare between the current |
| request and uploads in-progress. If they match, then a response |
| record is formed. |
| |
| ``match()`` |
| |
| This member function can be overriden to provide alternative |
| matching criteria. It takes two environments, the first |
| is the current request, the second is a current upload. |
| |
| ``report()`` |
| |
| This member function takes an environment and builds a |
| ``dict`` that will be used to create a JSON mapping for |
| the given upload. By default, this just includes the |
| percent complete and the request url. |
| |
| """ |
| def __init__(self, monitor): |
| self.monitor = monitor |
| |
| def match(self, search_environ, upload_environ): |
| if search_environ.get('REMOTE_USER', None) == \ |
| upload_environ.get('REMOTE_USER', 0): |
| return True |
| return False |
| |
| def report(self, environ): |
| retval = { 'started': time.strftime("%Y-%m-%d %H:%M:%S", |
| time.gmtime(environ[REQUEST_STARTED])), |
| 'finished': '', |
| 'content_length': environ.get('CONTENT_LENGTH'), |
| 'bytes_received': environ[ENVIRON_RECEIVED], |
| 'path_info': environ.get('PATH_INFO',''), |
| 'query_string': environ.get('QUERY_STRING','')} |
| finished = environ[REQUEST_FINISHED] |
| if finished: |
| retval['finished'] = time.strftime("%Y:%m:%d %H:%M:%S", |
| time.gmtime(finished)) |
| return retval |
| |
| def __call__(self, environ, start_response): |
| body = [] |
| for map in [self.report(env) for env in self.monitor.uploads() |
| if self.match(environ, env)]: |
| parts = [] |
| for k, v in map.items(): |
| v = str(v).replace("\\", "\\\\").replace('"', '\\"') |
| parts.append('%s: "%s"' % (k, v)) |
| body.append("{ %s }" % ", ".join(parts)) |
| body = "[ %s ]" % ", ".join(body) |
| start_response("200 OK", [('Content-Type', 'text/plain'), |
| ('Content-Length', len(body))]) |
| return [body] |
| |
| __all__ = ['UploadProgressMonitor', 'UploadProgressReporter'] |
| |
| if "__main__" == __name__: |
| import doctest |
| doctest.testmod(optionflags=doctest.ELLIPSIS) |