Skip to content

Commit 76dfd47

Browse files
committed
Add request.content_length, simplify MultiPartParser to use request only
1 parent 786b95b commit 76dfd47

File tree

3 files changed

+38
-69
lines changed

3 files changed

+38
-69
lines changed

plain/plain/http/multipartparser.py

Lines changed: 17 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from plain.utils.regex_helper import _lazy_re_compile
2929

3030
if TYPE_CHECKING:
31-
from plain.internal.files.uploadhandler import FileUploadHandler
31+
from plain.http.request import Request
3232

3333
__all__ = ("MultiPartParser", "MultiPartParserError", "InputStreamExhausted")
3434

@@ -61,28 +61,15 @@ class MultiPartParser:
6161

6262
boundary_re = _lazy_re_compile(r"[ -~]{0,200}[!-~]")
6363

64-
def __init__(
65-
self,
66-
environ: dict[str, Any],
67-
input_data: Any,
68-
upload_handlers: list[FileUploadHandler],
69-
encoding: str | None = None,
70-
):
64+
def __init__(self, request: Request):
7165
"""
7266
Initialize the MultiPartParser object.
7367
74-
:environ:
75-
The WSGI environ dictionary from the request.
76-
:input_data:
77-
The raw post data, as a file-like object.
78-
:upload_handlers:
79-
A list of UploadHandler instances that perform operations on the
80-
uploaded data.
81-
:encoding:
82-
The encoding with which to treat the incoming data.
68+
:request:
69+
The HTTP request object (used for headers and as the input stream).
8370
"""
8471
# Content-Type should contain multipart and the boundary information.
85-
content_type = environ.get("CONTENT_TYPE", "")
72+
content_type = request.content_type or ""
8673
if not content_type.startswith("multipart/"):
8774
raise MultiPartParserError(f"Invalid Content-Type: {content_type}")
8875

@@ -93,37 +80,30 @@ def __init__(
9380
f"Invalid non-ASCII Content-Type in multipart: {force_str(content_type)}"
9481
)
9582

96-
# Parse the header to get the boundary to split the parts.
97-
_, opts = parse_header_parameters(content_type)
98-
boundary = opts.get("boundary")
83+
# Get the boundary from parsed content type parameters.
84+
content_params = request.content_params or {}
85+
boundary = content_params.get("boundary")
9986
if not boundary or not self.boundary_re.fullmatch(boundary):
10087
raise MultiPartParserError(
10188
f"Invalid boundary in multipart: {force_str(boundary)}"
10289
)
10390

104-
# Content-Length should contain the length of the body we are about
105-
# to receive.
106-
try:
107-
content_length = int(environ.get("CONTENT_LENGTH", 0))
108-
except (ValueError, TypeError):
109-
content_length = 0
110-
91+
content_length = request.content_length
11192
if content_length < 0:
11293
# This means we shouldn't continue...raise an error.
11394
raise MultiPartParserError(f"Invalid content length: {content_length!r}")
11495

11596
self._boundary = boundary.encode("ascii")
116-
self._input_data = input_data
97+
self._request = request
11798

11899
# For compatibility with low-level network APIs (with 32-bit integers),
119100
# the chunk size should be < 2^31, but still divisible by 4.
120-
possible_sizes = [x.chunk_size for x in upload_handlers if x.chunk_size]
101+
possible_sizes = [x.chunk_size for x in request.upload_handlers if x.chunk_size]
121102
self._chunk_size = min([2**31 - 4] + possible_sizes)
122103

123-
self._environ = environ
124-
self._encoding = encoding or settings.DEFAULT_CHARSET
104+
self._encoding = request.encoding or settings.DEFAULT_CHARSET
125105
self._content_length = content_length
126-
self._upload_handlers = upload_handlers
106+
self._upload_handlers = request.upload_handlers
127107

128108
def parse(self) -> tuple[Any, MultiValueDict]:
129109
# Call the actual parse routine and close all open files in case of
@@ -162,9 +142,7 @@ def _parse(self) -> tuple[Any, MultiValueDict]:
162142
# This allows overriding everything if need be.
163143
for handler in handlers:
164144
result = handler.handle_raw_input(
165-
self._input_data,
166-
self._environ,
167-
self._content_length,
145+
self._request,
168146
self._boundary,
169147
encoding,
170148
)
@@ -177,7 +155,7 @@ def _parse(self) -> tuple[Any, MultiValueDict]:
177155
self._files = MultiValueDict()
178156

179157
# Instantiate the parser and stream:
180-
stream = LazyStream(ChunkIter(self._input_data, self._chunk_size))
158+
stream = LazyStream(ChunkIter(self._request, self._chunk_size))
181159

182160
# Whether or not to signal a file-completion at the beginning of the loop.
183161
old_field_name = None
@@ -366,13 +344,13 @@ def _parse(self) -> tuple[Any, MultiValueDict]:
366344
except StopUpload as e:
367345
self._close_files()
368346
if not e.connection_reset:
369-
exhaust(self._input_data)
347+
exhaust(self._request)
370348
else:
371349
if not uploaded_file:
372350
for handler in handlers:
373351
handler.upload_interrupted()
374352
# Make sure that the request data is all fed
375-
exhaust(self._input_data)
353+
exhaust(self._request)
376354

377355
# Signal that the upload has completed.
378356
# any() shortcircuits if a handler's upload_complete() returns a value.

plain/plain/http/request.py

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from functools import cached_property
99
from io import BytesIO
1010
from itertools import chain
11-
from typing import IO, TYPE_CHECKING, Any, TypeVar, overload
11+
from typing import TYPE_CHECKING, Any, TypeVar, overload
1212
from urllib.parse import parse_qsl, quote, urlencode, urljoin, urlsplit
1313

1414
if TYPE_CHECKING:
@@ -202,6 +202,14 @@ def query_string(self) -> str:
202202
"""Return the raw query string from the request URL."""
203203
return self.environ.get("QUERY_STRING", "")
204204

205+
@property
206+
def content_length(self) -> int:
207+
"""Return the Content-Length header value, or 0 if not provided."""
208+
try:
209+
return int(self.environ.get("CONTENT_LENGTH") or 0)
210+
except (ValueError, TypeError):
211+
return 0
212+
205213
def get_full_path(self, force_append_slash: bool = False) -> str:
206214
"""
207215
Return the full path for the request, including query string.
@@ -314,8 +322,7 @@ def body(self) -> bytes:
314322
# Limit the maximum request data size that will be handled in-memory.
315323
if (
316324
settings.DATA_UPLOAD_MAX_MEMORY_SIZE is not None
317-
and int(self.environ.get("CONTENT_LENGTH") or 0)
318-
> settings.DATA_UPLOAD_MAX_MEMORY_SIZE
325+
and self.content_length > settings.DATA_UPLOAD_MAX_MEMORY_SIZE
319326
):
320327
raise RequestDataTooBig(
321328
"Request body exceeded settings.DATA_UPLOAD_MAX_MEMORY_SIZE."
@@ -330,28 +337,14 @@ def body(self) -> bytes:
330337
self._stream = BytesIO(self._body)
331338
return self._body
332339

333-
def _parse_file_upload(
334-
self, environ: dict[str, Any], post_data: IO[bytes]
335-
) -> tuple[Any, MultiValueDict]:
336-
"""Return a tuple of (data QueryDict, files MultiValueDict)."""
337-
parser = MultiPartParser(
338-
environ, post_data, self.upload_handlers, self.encoding
339-
)
340-
return parser.parse()
341-
342340
@cached_property
343341
def _multipart_data(self) -> tuple[QueryDict, MultiValueDict]:
344342
"""Parse multipart/form-data. Used internally by form_data and files properties.
345343
346344
Raises MultiPartParserError or TooManyFilesSent for malformed uploads,
347345
which are handled by response_for_exception() as 400 errors.
348346
"""
349-
if hasattr(self, "_body"):
350-
# Use already read data
351-
data = BytesIO(self._body)
352-
else:
353-
data = self
354-
return self._parse_file_upload(self.environ, data)
347+
return MultiPartParser(self).parse()
355348

356349
@cached_property
357350
def json_data(self) -> dict[str, Any]:

plain/plain/internal/files/uploadhandler.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ class FileUploadHandler(ABC):
8686

8787
chunk_size = 64 * 2**10 # : The default chunk size is 64 KB.
8888

89-
def __init__(self, request: Request | None = None) -> None:
89+
def __init__(self, request: Request) -> None:
9090
self.file_name = None
9191
self.content_type = None
9292
self.content_length = None
@@ -97,8 +97,6 @@ def __init__(self, request: Request | None = None) -> None:
9797
def handle_raw_input(
9898
self,
9999
input_data: Any,
100-
environ: dict[str, Any],
101-
content_length: int,
102100
boundary: bytes,
103101
encoding: str | None = None,
104102
) -> None:
@@ -109,13 +107,13 @@ def handle_raw_input(
109107
110108
:input_data:
111109
An object that supports reading via .read().
112-
:environ:
113-
``request.environ``.
114-
:content_length:
115-
The (integer) value of the Content-Length header from the
116-
client.
117-
:boundary: The boundary from the Content-Type header. Be sure to
110+
:boundary:
111+
The boundary from the Content-Type header. Be sure to
118112
prepend two '--'.
113+
:encoding:
114+
The encoding of the request data.
115+
116+
Note: Access self.request for content_length, environ, or other request data.
119117
"""
120118
pass
121119

@@ -219,8 +217,6 @@ class MemoryFileUploadHandler(FileUploadHandler):
219217
def handle_raw_input(
220218
self,
221219
input_data: Any,
222-
environ: dict[str, Any],
223-
content_length: int,
224220
boundary: bytes,
225221
encoding: str | None = None,
226222
) -> None:
@@ -230,7 +226,9 @@ def handle_raw_input(
230226
"""
231227
# Check the content-length header to see if we should
232228
# If the post is too large, we cannot use the Memory handler.
233-
self.activated = content_length <= settings.FILE_UPLOAD_MAX_MEMORY_SIZE
229+
self.activated = (
230+
self.request.content_length <= settings.FILE_UPLOAD_MAX_MEMORY_SIZE
231+
)
234232

235233
def new_file(self, *args: Any, **kwargs: Any) -> None:
236234
super().new_file(*args, **kwargs)

0 commit comments

Comments
 (0)