summaryrefslogtreecommitdiff
path: root/paste/progress.py
diff options
context:
space:
mode:
authorcce <devnull@localhost>2006-01-11 15:34:25 +0000
committercce <devnull@localhost>2006-01-11 15:34:25 +0000
commitdd6c180701bb867bb768e770387a39cd9f117705 (patch)
treedb48f6782c73b0d59dd67242d29be5647adeb31d /paste/progress.py
parent88839390a6e317da438ca5f8dc03a827aa62ea1f (diff)
downloadpaste-dd6c180701bb867bb768e770387a39cd9f117705.tar.gz
- added paste.progress : a request upload progress monitor, partially complete (needs more testing, and a way to expire old uploaded requests)
- added paste.debug.debugapp which bootstraps with a simple HTML applicatoin, and a slow consumer (paste.wsgilib.dumpenviron needs to move there)
Diffstat (limited to 'paste/progress.py')
-rwxr-xr-xpaste/progress.py220
1 files changed, 220 insertions, 0 deletions
diff --git a/paste/progress.py b/paste/progress.py
new file mode 100755
index 0000000..f10ee27
--- /dev/null
+++ b/paste/progress.py
@@ -0,0 +1,220 @@
+# (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
+>>> 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 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:
+ size = len(chunk)
+ 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:
+ """
+ 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:
+ """
+ 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)