| 1 |
''' |
|---|
| 2 |
UploadFilter - File upload functionality. |
|---|
| 3 |
2006, James Kassemi - http://www.kepty.com |
|---|
| 4 |
|
|---|
| 5 |
If you allow users to upload files to your site you're definitely going to want |
|---|
| 6 |
to use the uploadfilter.max_concurrent setting, and set it to less than the |
|---|
| 7 |
number of threads in your server.thread_pool setting. Without it you'll be |
|---|
| 8 |
opening your site up to a simple dos if there are a number of concurrent |
|---|
| 9 |
file uploads that utilize all of your threads. |
|---|
| 10 |
|
|---|
| 11 |
As you'll be doing anyway, make sure that the enctype of your form is |
|---|
| 12 |
multipart/form-data, as that's what we'll be using to determine whether or not |
|---|
| 13 |
to track a file upload. |
|---|
| 14 |
|
|---|
| 15 |
Configuration: |
|---|
| 16 |
- uploadfilter.max_concurrent |
|---|
| 17 |
Set the number of files that can be concurrently uploaded to the site. |
|---|
| 18 |
If the number exceeds the number set here, Upload_MaxConcError will be |
|---|
| 19 |
raised. |
|---|
| 20 |
|
|---|
| 21 |
- uploadfilter.max_size |
|---|
| 22 |
Size, in kb, to limit uploaded files to. This will check both the |
|---|
| 23 |
header version, but in case that's spoofed, it will also check during |
|---|
| 24 |
the writing of the file to the temporary area. Raises |
|---|
| 25 |
Upload_MaxSizeError if the size exceeds this number. This will |
|---|
| 26 |
also override cherrypy.max_request_body_size for this area, so you don't |
|---|
| 27 |
have to worry about conflicting with that. If this is NOT set then |
|---|
| 28 |
you'll be dealing with the max_request_body_size, and we'll do NO |
|---|
| 29 |
checks. |
|---|
| 30 |
|
|---|
| 31 |
- uploadfilter.timeout |
|---|
| 32 |
Time cap. will raise Upload_TimeoutError if the user has been uploading a file |
|---|
| 33 |
for longer than the value set here. |
|---|
| 34 |
|
|---|
| 35 |
- uploadfilter.explicit |
|---|
| 36 |
Tells the system to check whether or not pages allows uploads. Set |
|---|
| 37 |
this at a root directory, and then add |
|---|
| 38 |
|
|---|
| 39 |
uploadfilter.declared=True |
|---|
| 40 |
|
|---|
| 41 |
where a page accepts file uploads. This prevents someone from posting file |
|---|
| 42 |
data to other fields, tying up your bandwidth by exploiting the fact cp |
|---|
| 43 |
will upload the file before you can check it. |
|---|
| 44 |
|
|---|
| 45 |
- uploadfilter.min_upspeed |
|---|
| 46 |
To keep someone from maintaining a connection and tying up a thread by |
|---|
| 47 |
uploading at a VERY slow rate, you can set this value (make sure it's |
|---|
| 48 |
somewhat low). It will raise Upload_UpSpeedError if the user's average |
|---|
| 49 |
upload speed drops below this value. the uploadfilter.timeout filter |
|---|
| 50 |
can be used as an alternative, but this might be preferable, depending |
|---|
| 51 |
on your situation. |
|---|
| 52 |
|
|---|
| 53 |
Real-time statistics: |
|---|
| 54 |
The 'file_transfers' attribute is added to the cherrypy object, and can be |
|---|
| 55 |
used to keep track of files being uploaded from a remote host. The format |
|---|
| 56 |
is as follows: |
|---|
| 57 |
|
|---|
| 58 |
cherrypy.file_transfers[remote_addr][filename] = ProgressFile object |
|---|
| 59 |
|
|---|
| 60 |
And the ProgressFile object will maintain these attributes: |
|---|
| 61 |
- transfered byte size of transfered data thus far. |
|---|
| 62 |
- speed bytes/sec |
|---|
| 63 |
- remaining bytes remaining |
|---|
| 64 |
- eta estimated seconds until arrival |
|---|
| 65 |
|
|---|
| 66 |
It's possible to create an AJAX-style interface to show the user the status |
|---|
| 67 |
of their file uploads now, so long as you have an available thread to take |
|---|
| 68 |
the requests for it... |
|---|
| 69 |
''' |
|---|
| 70 |
|
|---|
| 71 |
import cgi |
|---|
| 72 |
import cherrypy |
|---|
| 73 |
import tempfile |
|---|
| 74 |
import time |
|---|
| 75 |
|
|---|
| 76 |
from cherrypy.filters.basefilter import BaseFilter |
|---|
| 77 |
|
|---|
| 78 |
class Upload_MaxConcError(Exception): |
|---|
| 79 |
pass |
|---|
| 80 |
class Upload_TimeoutError(Exception): |
|---|
| 81 |
pass |
|---|
| 82 |
class Upload_MaxSizeError(Exception): |
|---|
| 83 |
pass |
|---|
| 84 |
class Upload_UnauthorizedError(Exception): |
|---|
| 85 |
pass |
|---|
| 86 |
class Upload_UpSpeedError(Exception): |
|---|
| 87 |
pass |
|---|
| 88 |
|
|---|
| 89 |
current_uploads = 0 |
|---|
| 90 |
cherrypy.file_transfers = dict() |
|---|
| 91 |
|
|---|
| 92 |
class ProgressFile(object): |
|---|
| 93 |
def __init__(self, buf, *args, **kwargs): |
|---|
| 94 |
self.file_object = tempfile.TemporaryFile( |
|---|
| 95 |
*args, **kwargs) |
|---|
| 96 |
self.transfered = 0 |
|---|
| 97 |
self.buf = buf |
|---|
| 98 |
self.pre_sized = float(cherrypy.request.headers['Content-length']) |
|---|
| 99 |
self.speed = 1 |
|---|
| 100 |
self.remaining = 0 |
|---|
| 101 |
self.eta = 0 |
|---|
| 102 |
self._start = time.time() |
|---|
| 103 |
|
|---|
| 104 |
def write(self, data): |
|---|
| 105 |
now = time.time() |
|---|
| 106 |
self.transfered += len(data) |
|---|
| 107 |
upload_timeout = getattr(cherrypy.thread_data, 'upload_timeout', False) |
|---|
| 108 |
if upload_timeout: |
|---|
| 109 |
if (now - self._start) > upload_timeout: |
|---|
| 110 |
raise Upload_TimeoutError |
|---|
| 111 |
|
|---|
| 112 |
upload_maxsize = getattr(cherrypy.thread_data, 'upload_maxsize', False) |
|---|
| 113 |
if upload_maxsize: |
|---|
| 114 |
if self.transfered > upload_maxsize: |
|---|
| 115 |
raise Upload_MaxSizeError |
|---|
| 116 |
|
|---|
| 117 |
self.speed = self.transfered / (now - self._start) |
|---|
| 118 |
|
|---|
| 119 |
upload_minspeed = getattr(cherrypy.thread_data, 'upload_minspeed', False) |
|---|
| 120 |
if upload_minspeed: |
|---|
| 121 |
if self.transfered > (5 * self.buf): |
|---|
| 122 |
if self.speed < upload_minspeed: |
|---|
| 123 |
raise Upload_UpSpeedError |
|---|
| 124 |
|
|---|
| 125 |
self.remaining = self.pre_sized - self.transfered |
|---|
| 126 |
|
|---|
| 127 |
if self.speed == 0: self.eta = 9999999 |
|---|
| 128 |
else: self.eta = self.remaining / self.speed |
|---|
| 129 |
|
|---|
| 130 |
return self.file_object.write(data) |
|---|
| 131 |
|
|---|
| 132 |
def seek(self, pos): |
|---|
| 133 |
self.post_sized = self.transfered |
|---|
| 134 |
self.transfered = True |
|---|
| 135 |
return self.file_object.seek(pos) |
|---|
| 136 |
|
|---|
| 137 |
def read(self, size): |
|---|
| 138 |
return self.file_object.read(size) |
|---|
| 139 |
|
|---|
| 140 |
class FieldStorage(cherrypy._cpcgifs.FieldStorage): |
|---|
| 141 |
''' We want control over our timing and download status, |
|---|
| 142 |
so we've got to override the original. This will work |
|---|
| 143 |
transparently without interfering with the user, but |
|---|
| 144 |
might warrant addition to _cpcgifs ''' |
|---|
| 145 |
|
|---|
| 146 |
def __del__(self, *args, **kwargs): |
|---|
| 147 |
try: |
|---|
| 148 |
dcopy = cherrypy.file_transfers[cherrypy.request.remote_addr].copy() |
|---|
| 149 |
for key, val in dcopy.iteritems(): |
|---|
| 150 |
if val.transfered == True: |
|---|
| 151 |
del cherrypy.file_transfers[cherrypy.request.remote_addr][key] |
|---|
| 152 |
del dcopy |
|---|
| 153 |
if len(cherrypy.file_transfers[cherrypy.request.remote_addr]) == 0: |
|---|
| 154 |
del cherrypy.file_transfers[cherrypy.request.remote_addr] |
|---|
| 155 |
|
|---|
| 156 |
except KeyError: |
|---|
| 157 |
pass |
|---|
| 158 |
|
|---|
| 159 |
def make_file(self, binary=None): |
|---|
| 160 |
fo = ProgressFile(self.bufsize) |
|---|
| 161 |
if cherrypy.file_transfers.has_key(cherrypy.request.remote_addr): |
|---|
| 162 |
cherrypy.file_transfers[cherrypy.request.remote_addr]\ |
|---|
| 163 |
[self.filename] = fo |
|---|
| 164 |
else: |
|---|
| 165 |
cherrypy.file_transfers[cherrypy.request.remote_addr]\ |
|---|
| 166 |
= {self.filename:fo} |
|---|
| 167 |
|
|---|
| 168 |
return fo |
|---|
| 169 |
|
|---|
| 170 |
cherrypy._cpcgifs.FieldStorage = FieldStorage |
|---|
| 171 |
|
|---|
| 172 |
class UploadFilter(BaseFilter): |
|---|
| 173 |
|
|---|
| 174 |
|
|---|
| 175 |
|
|---|
| 176 |
|
|---|
| 177 |
def before_request_body(self): |
|---|
| 178 |
global current_uploads |
|---|
| 179 |
|
|---|
| 180 |
if not cherrypy.config.get('upload_filter.on', False): |
|---|
| 181 |
return |
|---|
| 182 |
|
|---|
| 183 |
if cherrypy.request.headers.get('Content-Type', '').split(';')[0] ==\ |
|---|
| 184 |
'multipart/form-data': |
|---|
| 185 |
|
|---|
| 186 |
upload_explicit = cherrypy.config.get('upload_filter.explicit', False) |
|---|
| 187 |
upload_declared = cherrypy.config.get('upload_filter.declared', False) |
|---|
| 188 |
upload_limit = cherrypy.config.get('upload_filter.max_concurrent', False) |
|---|
| 189 |
upload_timeout = cherrypy.config.get('upload_filter.timeout', False) |
|---|
| 190 |
upload_maxsize = cherrypy.config.get('upload_filter.max_size', False) |
|---|
| 191 |
upload_minspeed = cherrypy.config.get('upload_filter.min_upspeed', False) |
|---|
| 192 |
|
|---|
| 193 |
|
|---|
| 194 |
cherrypy.thread_data.upload_minspeed = upload_minspeed |
|---|
| 195 |
|
|---|
| 196 |
if upload_explicit and not upload_declared: |
|---|
| 197 |
raise Upload_UnauthorizedError |
|---|
| 198 |
|
|---|
| 199 |
if upload_limit: |
|---|
| 200 |
if current_uploads > upload_limit: |
|---|
| 201 |
raise Upload_MaxConcError |
|---|
| 202 |
current_uploads += 1 |
|---|
| 203 |
|
|---|
| 204 |
if upload_timeout: |
|---|
| 205 |
cherrypy.thread_data.upload_timeout = upload_timeout |
|---|
| 206 |
|
|---|
| 207 |
if upload_maxsize: |
|---|
| 208 |
upload_maxsize *= 1024 |
|---|
| 209 |
cherrypy.thread_data.upload_maxsize = upload_maxsize |
|---|
| 210 |
size = float(cherrypy.request.headers['Content-length']) |
|---|
| 211 |
if size > upload_maxsize: |
|---|
| 212 |
raise Upload_MaxSizeError |
|---|
| 213 |
cherrypy.request.rfile = cherrypy.request.rfile.rfile |
|---|
| 214 |
|
|---|
| 215 |
def on_end_resource(self): |
|---|
| 216 |
global current_uploads |
|---|
| 217 |
|
|---|
| 218 |
if not cherrypy.config.get('upload_limit_filter.on', False): |
|---|
| 219 |
return |
|---|
| 220 |
|
|---|
| 221 |
if cherrypy.request.headers.get('Content-Type', '').split(';')[0] ==\ |
|---|
| 222 |
'multipart/form-data': |
|---|
| 223 |
|
|---|
| 224 |
upload_explicit = cherrypy.config.get('upload_filter.explicit', |
|---|
| 225 |
False) |
|---|
| 226 |
upload_declared = cherrypy.config.get('upload_filter.declared', |
|---|
| 227 |
False) |
|---|
| 228 |
upload_limit = cherrypy.config.get('upload_filter.max_concurrent', |
|---|
| 229 |
False) |
|---|
| 230 |
upload_timeout = cherrypy.config.get('upload_filter.timeout', |
|---|
| 231 |
False) |
|---|
| 232 |
upload_maxsize = cherrypy.config.get('upload_filter.max_size', |
|---|
| 233 |
False) |
|---|
| 234 |
|
|---|
| 235 |
if upload_explicit and not upload_declared: |
|---|
| 236 |
return |
|---|
| 237 |
|
|---|
| 238 |
if upload_limit: |
|---|
| 239 |
current_uploads -= 1 |
|---|
| 240 |
|
|---|
| 241 |
if upload_timeout: |
|---|
| 242 |
del cherrypy.thread_data.upload_timeout |
|---|
| 243 |
|
|---|
| 244 |
if upload_maxsize: |
|---|
| 245 |
del cherrypy.thread_data.upload_maxsize |
|---|