| 2348 | |
| 2349 | |
| 2350 | class FileUpload(object): |
| 2351 | |
| 2352 | def __init__(self, fileobj, name, filename, headers=None): |
| 2353 | ''' Wrapper for file uploads. ''' |
| 2354 | #: Open file(-like) object (BytesIO buffer or temporary file) |
| 2355 | self.file = fileobj |
| 2356 | #: Name of the upload form field |
| 2357 | self.name = name |
| 2358 | #: Raw filename as sent by the client (may contain unsafe characters) |
| 2359 | self.raw_filename = filename |
| 2360 | #: A :class:`HeaderDict` with additional headers (e.g. content-type) |
| 2361 | self.headers = HeaderDict(headers) if headers else HeaderDict() |
| 2362 | |
| 2363 | content_type = HeaderProperty('Content-Type') |
| 2364 | content_length = HeaderProperty('Content-Length', reader=int, default=-1) |
| 2365 | |
| 2366 | def get_header(self, name, default=None): |
| 2367 | """ Return the value of a header within the mulripart part. """ |
| 2368 | return self.headers.get(name, default) |
| 2369 | |
| 2370 | @cached_property |
| 2371 | def filename(self): |
| 2372 | ''' Name of the file on the client file system, but normalized to ensure |
| 2373 | file system compatibility. An empty filename is returned as 'empty'. |
| 2374 | |
| 2375 | Only ASCII letters, digits, dashes, underscores and dots are |
| 2376 | allowed in the final filename. Accents are removed, if possible. |
| 2377 | Whitespace is replaced by a single dash. Leading or tailing dots |
| 2378 | or dashes are removed. The filename is limited to 255 characters. |
| 2379 | ''' |
| 2380 | fname = self.raw_filename |
| 2381 | if not isinstance(fname, unicode): |
| 2382 | fname = fname.decode('utf8', 'ignore') |
| 2383 | fname = normalize('NFKD', fname).encode('ASCII', 'ignore').decode('ASCII') |
| 2384 | fname = os.path.basename(fname.replace('\\', os.path.sep)) |
| 2385 | fname = re.sub(r'[^a-zA-Z0-9-_.\s]', '', fname).strip() |
| 2386 | fname = re.sub(r'[-\s]+', '-', fname).strip('.-') |
| 2387 | return fname[:255] or 'empty' |
| 2388 | |
| 2389 | def _copy_file(self, fp, chunk_size=2**16): |
| 2390 | read, write, offset = self.file.read, fp.write, self.file.tell() |
| 2391 | while 1: |
| 2392 | buf = read(chunk_size) |
| 2393 | if not buf: break |
| 2394 | write(buf) |
| 2395 | self.file.seek(offset) |
| 2396 | |
| 2397 | def save(self, destination, overwrite=False, chunk_size=2**16): |
| 2398 | ''' Save file to disk or copy its content to an open file(-like) object. |
| 2399 | If *destination* is a directory, :attr:`filename` is added to the |
| 2400 | path. Existing files are not overwritten by default (IOError). |
| 2401 | |
| 2402 | :param destination: File path, directory or file(-like) object. |
| 2403 | :param overwrite: If True, replace existing files. (default: False) |
| 2404 | :param chunk_size: Bytes to read at a time. (default: 64kb) |
| 2405 | ''' |
| 2406 | if isinstance(destination, basestring): # Except file-likes here |
| 2407 | if os.path.isdir(destination): |