diff --git a/docs/requests.md b/docs/requests.md
index 747e496d1..11fb58343 100644
--- a/docs/requests.md
+++ b/docs/requests.md
@@ -123,6 +123,7 @@ multidict, containing both file uploads and text input. File upload items are re
* `content_type`: A `str` with the content type (MIME type / media type) (e.g. `image/jpeg`).
* `file`: A `SpooledTemporaryFile` (a file-like object). This is the actual Python file that you can pass directly to other functions or libraries that expect a "file-like" object.
* `headers`: A `Headers` object. Often this will only be the `Content-Type` header, but if additional headers were included in the multipart field they will be included here. Note that these headers have no relationship with the headers in `Request.headers`.
+* `size`: An `int` with file's size in bytes.
`UploadFile` has the following `async` methods. They all call the corresponding file methods underneath (using the internal `SpooledTemporaryFile`).
diff --git a/starlette/datastructures.py b/starlette/datastructures.py
index eee3834e0..00c9810e4 100644
--- a/starlette/datastructures.py
+++ b/starlette/datastructures.py
@@ -430,12 +430,14 @@ class UploadFile:
def __init__(
self,
file: typing.BinaryIO,
+ size: int,
*,
filename: typing.Optional[str] = None,
headers: "typing.Optional[Headers]" = None,
) -> None:
self.filename = filename
self.file = file
+ self.size = size
self.headers = headers or Headers()
@property
@@ -449,6 +451,8 @@ def _in_memory(self) -> bool:
return not rolled_to_disk
async def write(self, data: bytes) -> None:
+ self.size += len(data)
+
if self._in_memory:
self.file.write(data)
else:
diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py
index 16f9da4a5..0271cfd08 100644
--- a/tests/test_datastructures.py
+++ b/tests/test_datastructures.py
@@ -275,10 +275,12 @@ def test_queryparams():
async def test_upload_file_file_input():
"""Test passing file/stream into the UploadFile constructor"""
stream = io.BytesIO(b"data")
- file = UploadFile(filename="file", file=stream)
+ file = UploadFile(filename="file", file=stream, size=len(stream))
+ assert file.size == 4
assert await file.read() == b"data"
await file.write(b" and more data!")
assert await file.read() == b""
+ assert file.size == 19
await file.seek(0)
assert await file.read() == b"data and more data!"
@@ -292,7 +294,7 @@ async def test_uploadfile_rolling(max_size: int) -> None:
stream: BinaryIO = SpooledTemporaryFile( # type: ignore[assignment]
max_size=max_size
)
- file = UploadFile(filename="file", file=stream)
+ file = UploadFile(filename="file", file=stream, size=len(stream))
assert await file.read() == b""
await file.write(b"data")
assert await file.read() == b""
@@ -307,7 +309,7 @@ async def test_uploadfile_rolling(max_size: int) -> None:
def test_formdata():
stream = io.BytesIO(b"data")
- upload = UploadFile(filename="file", file=stream)
+ upload = UploadFile(filename="file", file=stream, size=len(stream))
form = FormData([("a", "123"), ("a", "456"), ("b", upload)])
assert "a" in form
assert "A" not in form