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