| 3126 | |
| 3127 | |
| 3128 | class FileUpload(object): |
| 3129 | def __init__(self, fileobj, name, filename, headers=None): |
| 3130 | """ Wrapper for file uploads. """ |
| 3131 | #: Open file(-like) object (BytesIO buffer or temporary file) |
| 3132 | self.file = fileobj |
| 3133 | #: Name of the upload form field |
| 3134 | self.name = name |
| 3135 | #: Raw filename as sent by the client (may contain unsafe characters) |
| 3136 | self.raw_filename = filename |
| 3137 | #: A :class:`HeaderDict` with additional headers (e.g. content-type) |
| 3138 | self.headers = HeaderDict(headers) if headers else HeaderDict() |
| 3139 | |
| 3140 | content_type = HeaderProperty('Content-Type') |
| 3141 | content_length = HeaderProperty('Content-Length', reader=int, default=-1) |
| 3142 | |
| 3143 | def get_header(self, name, default=None): |
| 3144 | """ Return the value of a header within the multipart part. """ |
| 3145 | return self.headers.get(name, default) |
| 3146 | |
| 3147 | @cached_property |
| 3148 | def filename(self): |
| 3149 | """ Name of the file on the client file system, but normalized to ensure |
| 3150 | file system compatibility. An empty filename is returned as 'empty'. |
| 3151 | |
| 3152 | Only ASCII letters, digits, dashes, underscores and dots are |
| 3153 | allowed in the final filename. Accents are removed, if possible. |
| 3154 | Whitespace is replaced by a single dash. Leading or tailing dots |
| 3155 | or dashes are removed. The filename is limited to 255 characters. |
| 3156 | """ |
| 3157 | fname = self.raw_filename |
| 3158 | if not isinstance(fname, unicode): |
| 3159 | fname = fname.decode('utf8', 'ignore') |
| 3160 | fname = normalize('NFKD', fname) |
| 3161 | fname = fname.encode('ASCII', 'ignore').decode('ASCII') |
| 3162 | fname = os.path.basename(fname.replace('\\', os.path.sep)) |
| 3163 | fname = re.sub(r'[^a-zA-Z0-9-_.\s]', '', fname).strip() |
| 3164 | fname = re.sub(r'[-\s]+', '-', fname).strip('.-') |
| 3165 | return fname[:255] or 'empty' |
| 3166 | |
| 3167 | def _copy_file(self, fp, chunk_size=2 ** 16): |
| 3168 | read, write, offset = self.file.read, fp.write, self.file.tell() |
| 3169 | while 1: |
| 3170 | buf = read(chunk_size) |
| 3171 | if not buf: break |
| 3172 | write(buf) |
| 3173 | self.file.seek(offset) |
| 3174 | |
| 3175 | def save(self, destination, overwrite=False, chunk_size=2 ** 16): |
| 3176 | """ Save file to disk or copy its content to an open file(-like) object. |
| 3177 | If *destination* is a directory, :attr:`filename` is added to the |
| 3178 | path. Existing files are not overwritten by default (IOError). |
| 3179 | |
| 3180 | :param destination: File path, directory or file(-like) object. |
| 3181 | :param overwrite: If True, replace existing files. (default: False) |
| 3182 | :param chunk_size: Bytes to read at a time. (default: 64kb) |
| 3183 | """ |
| 3184 | if isinstance(destination, basestring): # Except file-likes here |
| 3185 | if os.path.isdir(destination): |
no test coverage detected
searching dependent graphs…