| 2728 | |
| 2729 | |
| 2730 | class FileUpload(object): |
| 2731 | def __init__(self, fileobj, name, filename, headers=None): |
| 2732 | """ Wrapper for file uploads. """ |
| 2733 | #: Open file(-like) object (BytesIO buffer or temporary file) |
| 2734 | self.file = fileobj |
| 2735 | #: Name of the upload form field |
| 2736 | self.name = name |
| 2737 | #: Raw filename as sent by the client (may contain unsafe characters) |
| 2738 | self.raw_filename = filename |
| 2739 | #: A :class:`HeaderDict` with additional headers (e.g. content-type) |
| 2740 | self.headers = HeaderDict(headers) if headers else HeaderDict() |
| 2741 | |
| 2742 | content_type = HeaderProperty('Content-Type') |
| 2743 | content_length = HeaderProperty('Content-Length', reader=int, default=-1) |
| 2744 | |
| 2745 | def get_header(self, name, default=None): |
| 2746 | """ Return the value of a header within the multipart part. """ |
| 2747 | return self.headers.get(name, default) |
| 2748 | |
| 2749 | @cached_property |
| 2750 | def filename(self): |
| 2751 | """ Name of the file on the client file system, but normalized to ensure |
| 2752 | file system compatibility. An empty filename is returned as 'empty'. |
| 2753 | |
| 2754 | Only ASCII letters, digits, dashes, underscores and dots are |
| 2755 | allowed in the final filename. Accents are removed, if possible. |
| 2756 | Whitespace is replaced by a single dash. Leading or tailing dots |
| 2757 | or dashes are removed. The filename is limited to 255 characters. |
| 2758 | """ |
| 2759 | fname = self.raw_filename |
| 2760 | if not isinstance(fname, unicode): |
| 2761 | fname = fname.decode('utf8', 'ignore') |
| 2762 | fname = normalize('NFKD', fname) |
| 2763 | fname = fname.encode('ASCII', 'ignore').decode('ASCII') |
| 2764 | fname = os.path.basename(fname.replace('\\', os.path.sep)) |
| 2765 | fname = re.sub(r'[^a-zA-Z0-9-_.\s]', '', fname).strip() |
| 2766 | fname = re.sub(r'[-\s]+', '-', fname).strip('.-') |
| 2767 | return fname[:255] or 'empty' |
| 2768 | |
| 2769 | def _copy_file(self, fp, chunk_size=2 ** 16): |
| 2770 | read, write, offset = self.file.read, fp.write, self.file.tell() |
| 2771 | while 1: |
| 2772 | buf = read(chunk_size) |
| 2773 | if not buf: break |
| 2774 | write(buf) |
| 2775 | self.file.seek(offset) |
| 2776 | |
| 2777 | def save(self, destination, overwrite=False, chunk_size=2 ** 16): |
| 2778 | """ Save file to disk or copy its content to an open file(-like) object. |
| 2779 | If *destination* is a directory, :attr:`filename` is added to the |
| 2780 | path. Existing files are not overwritten by default (IOError). |
| 2781 | |
| 2782 | :param destination: File path, directory or file(-like) object. |
| 2783 | :param overwrite: If True, replace existing files. (default: False) |
| 2784 | :param chunk_size: Bytes to read at a time. (default: 64kb) |
| 2785 | """ |
| 2786 | if isinstance(destination, basestring): # Except file-likes here |
| 2787 | if os.path.isdir(destination): |