Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upload tarfile file-like objects as data attributes #6747

Merged
merged 9 commits into from
Aug 30, 2024
Merged
1 change: 1 addition & 0 deletions CHANGES/6732.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed handling of some file-like objects (e.g. ``tarfile.extractfile()``) which raise ``AttributeError`` instead of ``OSError`` when ``fileno`` fails for streaming payload data -- by :user:`ReallyReivax`.
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ William Grzybowski
William S.
Wilson Ong
wouter bolsterlee
Xavier Halloran
Xiang Li
Yang Zhou
Yannick Koechlin
Expand Down
4 changes: 3 additions & 1 deletion aiohttp/payload.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,9 +398,11 @@ class BufferedReaderPayload(IOBasePayload):
def size(self) -> Optional[int]:
try:
return os.fstat(self._value.fileno()).st_size - self._value.tell()
except OSError:
except (OSError, AttributeError):
# data.fileno() is not supported, e.g.
# io.BufferedReader(io.BytesIO(b'data'))
# For some file-like objects (e.g. tarfile), the fileno() attribute may
# not exist at all, and will instead raise an AttributeError.
return None

def decode(self, encoding: str = "utf-8", errors: str = "strict") -> str:
Expand Down
57 changes: 57 additions & 0 deletions tests/test_client_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
import socket
import ssl
import sys
import tarfile
import time
import zipfile
from typing import (
Any,
AsyncIterator,
Expand Down Expand Up @@ -542,6 +544,61 @@ async def handler(request: web.Request) -> web.Response:
assert 200 == resp.status


async def test_post_data_zipfile_filelike(aiohttp_client: AiohttpClient) -> None:
data = b"This is a zip file payload text file."

async def handler(request: web.Request) -> web.Response:
val = await request.read()
assert data == val, "Transmitted zipfile member failed to match original data."
return web.Response()

app = web.Application()
app.router.add_route("POST", "/", handler)
client = await aiohttp_client(app)

buf = io.BytesIO()
with zipfile.ZipFile(file=buf, mode="w") as zf:
with zf.open("payload1.txt", mode="w") as zip_filelike_writing:
zip_filelike_writing.write(data)

buf.seek(0)
zf = zipfile.ZipFile(file=buf, mode="r")
resp = await client.post("/", data=zf.open("payload1.txt"))
assert 200 == resp.status


async def test_post_data_tarfile_filelike(aiohttp_client: AiohttpClient) -> None:
data = b"This is a tar file payload text file."

async def handler(request: web.Request) -> web.Response:
val = await request.read()
assert data == val, "Transmitted tarfile member failed to match original data."
return web.Response()

app = web.Application()
app.router.add_route("POST", "/", handler)
client = await aiohttp_client(app)

buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w") as tf:
ti = tarfile.TarInfo(name="payload1.txt")
ti.size = len(data)
tf.addfile(tarinfo=ti, fileobj=io.BytesIO(data))

# Random-access tarfile.
buf.seek(0)
tf = tarfile.open(fileobj=buf, mode="r:")
resp = await client.post("/", data=tf.extractfile("payload1.txt"))
assert 200 == resp.status

# Streaming tarfile.
buf.seek(0)
tf = tarfile.open(fileobj=buf, mode="r|")
for entry in tf:
resp = await client.post("/", data=tf.extractfile(entry))
assert 200 == resp.status


async def test_ssl_client(
aiohttp_server: AiohttpServer,
ssl_ctx: ssl.SSLContext,
Expand Down
Loading