| 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) |
|---|