|
11 | 11 | import anyio |
12 | 12 | from anyio import SpooledTemporaryFile |
13 | 13 |
|
| 14 | +from lilya.contrib.multipart import parsers as multipart |
| 15 | +from lilya.contrib.multipart.utils import _decode_rfc5987, parse_options_header |
14 | 16 | from lilya.datastructures import DataUpload, FormData, Header |
15 | 17 | from lilya.enums import FormMessage |
16 | 18 |
|
17 | | -try: |
18 | | - import python_multipart as multipart |
19 | | - from python_multipart.multipart import parse_options_header |
20 | | -except ModuleNotFoundError: # pragma: nocover |
21 | | - # old import name |
22 | | - try: |
23 | | - import multipart # type: ignore[no-redef] |
24 | | - from multipart.multipart import parse_options_header # type: ignore[no-redef] |
25 | | - except ModuleNotFoundError: # pragma: nocover |
26 | | - parse_options_header = None |
27 | | - multipart = None |
28 | | - |
29 | 19 |
|
30 | 20 | @lru_cache(1024) |
31 | 21 | def cookie_parser(cookie_string: str | bytes) -> dict[str, str]: |
@@ -160,7 +150,17 @@ async def parse(self) -> FormData: |
160 | 150 | "on_end": self.on_end, |
161 | 151 | } |
162 | 152 |
|
163 | | - parser = multipart.QuerystringParser(callbacks) |
| 153 | + parser = multipart.QuerystringParser() |
| 154 | + if hasattr(parser, "set_callback"): |
| 155 | + for cb_name, func in callbacks.items(): |
| 156 | + name = cb_name[3:] if cb_name.startswith("on_") else cb_name |
| 157 | + try: |
| 158 | + parser.set_callback(name, func) |
| 159 | + except Exception: # noqa |
| 160 | + pass |
| 161 | + elif hasattr(parser, "callbacks") and isinstance(parser.callbacks, dict): |
| 162 | + parser.callbacks.update(callbacks) |
| 163 | + |
164 | 164 | field_name = b"" |
165 | 165 | field_value = b"" |
166 | 166 | items: list[tuple[str, str | DataUpload]] = [] |
@@ -319,30 +319,31 @@ def on_headers_finished(self) -> None: |
319 | 319 | self._handle_no_filename() |
320 | 320 |
|
321 | 321 | def _set_field_name(self, options: dict[bytes, bytes]) -> None: |
322 | | - """ |
323 | | - Set the field name based on options in Content-Disposition header. |
324 | | -
|
325 | | - Args: |
326 | | - options (Dict[bytes, bytes]): Parsed options from the Content-Disposition header. |
327 | | - """ |
328 | | - try: |
| 322 | + """Set the field name based on options; support RFC5987 name*.""" |
| 323 | + if b"name" in options: |
329 | 324 | self._current_part.field_name = _user_safe_decode(options[b"name"], self._charset) |
330 | | - except KeyError: |
331 | | - raise MultiPartException( |
332 | | - 'The Content-Disposition header field "name" must be provided.' |
333 | | - ) from None |
| 325 | + return |
| 326 | + # RFC5987 name* |
| 327 | + raw = options.get(b"name*") |
| 328 | + if raw is not None: |
| 329 | + decoded = _decode_rfc5987(raw, self._charset) |
| 330 | + self._current_part.field_name = decoded |
| 331 | + return |
| 332 | + raise MultiPartException( |
| 333 | + 'The Content-Disposition header field "name" must be provided.' |
| 334 | + ) from None |
334 | 335 |
|
335 | 336 | def _handle_filename(self, options: dict[bytes, bytes]) -> None: |
336 | | - """ |
337 | | - Handle the case when the part has a filename. |
338 | | -
|
339 | | - Args: |
340 | | - options (Dict[bytes, bytes]): Parsed options from the Content-Disposition header. |
341 | | - """ |
| 337 | + """Handle the case when the part has a filename. Support RFC5987 filename*.""" |
342 | 338 | self._current_files += 1 |
343 | 339 | self._validate_files_count() |
344 | 340 |
|
345 | | - filename = _user_safe_decode(options[b"filename"], self._charset) |
| 341 | + filename = "" |
| 342 | + if b"filename" in options: |
| 343 | + filename = _user_safe_decode(options[b"filename"], self._charset) |
| 344 | + elif b"filename*" in options: |
| 345 | + filename = _decode_rfc5987(options[b"filename*"], self._charset) |
| 346 | + |
346 | 347 | tempfile = self._create_temp_file() |
347 | 348 | self._current_part.file = self._create_upload_file(filename, tempfile) |
348 | 349 |
|
@@ -483,15 +484,26 @@ def _create_multipart_parser( |
483 | 484 | ) -> multipart.MultipartParser: |
484 | 485 | """ |
485 | 486 | Create the multipart parser with the specified boundary and callbacks. |
486 | | -
|
487 | | - Args: |
488 | | - boundary (bytes): Multipart boundary. |
489 | | - callbacks (Dict[str, Callable]): Callbacks dictionary. |
490 | | -
|
491 | | - Returns: |
492 | | - multipart.MultipartParser: Created multipart parser. |
493 | | - """ |
494 | | - return multipart.MultipartParser(boundary, cast(Any, callbacks)) |
| 487 | + Works with lilya.contrib.multipart (preferred). |
| 488 | + """ |
| 489 | + # Our contrib parser signature is MultipartParser(boundary, *[, max_size=...]) |
| 490 | + mp = multipart.MultipartParser(boundary) |
| 491 | + # If it exposes set_callback(name, func) use that, stripping 'on_' |
| 492 | + if hasattr(mp, "set_callback"): |
| 493 | + for cb_name, func in callbacks.items(): |
| 494 | + name = cb_name[3:] if cb_name.startswith("on_") else cb_name |
| 495 | + try: |
| 496 | + mp.set_callback(name, func) |
| 497 | + except Exception: # noqa |
| 498 | + pass |
| 499 | + # Or store into callbacks dict if present |
| 500 | + if hasattr(mp, "callbacks") and isinstance(mp.callbacks, dict): |
| 501 | + for cb_name, func in callbacks.items(): |
| 502 | + try: |
| 503 | + mp.callbacks[cb_name] = func |
| 504 | + except Exception: # noqa |
| 505 | + pass |
| 506 | + return mp |
495 | 507 |
|
496 | 508 | async def _write_file_data(self) -> None: |
497 | 509 | """ |
|
0 commit comments