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