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""" 8Upload Progress Monitor 9 10This is a WSGI middleware component which monitors the status of files 11being uploaded. It includes a small query application which will return 12a 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)) 28serving on... 29 30.. note:: 31 32 This is experimental, and will change in the future. 33""" 34import time 35from paste.wsgilib import catch_errors 36 37DEFAULT_THRESHOLD = 1024 * 1024 # one megabyte 38DEFAULT_TIMEOUT = 60*5 # five minutes 39ENVIRON_RECEIVED = 'paste.bytes_received' 40REQUEST_STARTED = 'paste.request_started' 41REQUEST_FINISHED = 'paste.request_finished' 42 43class _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 80class 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 157class 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 220if "__main__" == __name__: 221 import doctest 222 doctest.testmod(optionflags=doctest.ELLIPSIS) 223