Skip to content

Commit 420c242

Browse files
authored
Merge pull request #194 from justmobilize/post-file-as-data
Add ability to post a file via the data param
2 parents fac4012 + 29ada91 commit 420c242

File tree

2 files changed

+61
-23
lines changed

2 files changed

+61
-23
lines changed

adafruit_requests.py

+38-22
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050

5151
if not sys.implementation.name == "circuitpython":
5252
from types import TracebackType
53-
from typing import Any, Dict, Optional, Type
53+
from typing import IO, Any, Dict, Optional, Type
5454

5555
from circuitpython_typing.socket import (
5656
SocketpoolModuleType,
@@ -387,19 +387,7 @@ def _build_boundary_data(self, files: dict): # pylint: disable=too-many-locals
387387
boundary_objects.append("\r\n")
388388

389389
if hasattr(file_handle, "read"):
390-
is_binary = False
391-
try:
392-
content = file_handle.read(1)
393-
is_binary = isinstance(content, bytes)
394-
except UnicodeError:
395-
is_binary = False
396-
397-
if not is_binary:
398-
raise ValueError("Files must be opened in binary mode")
399-
400-
file_handle.seek(0, SEEK_END)
401-
content_length += file_handle.tell()
402-
file_handle.seek(0)
390+
content_length += self._get_file_length(file_handle)
403391

404392
boundary_objects.append(file_handle)
405393
boundary_objects.append("\r\n")
@@ -428,6 +416,25 @@ def _check_headers(headers: Dict[str, str]):
428416
f"Header part ({value}) from {key} must be of type str or bytes, not {type(value)}"
429417
)
430418

419+
@staticmethod
420+
def _get_file_length(file_handle: IO):
421+
is_binary = False
422+
try:
423+
file_handle.seek(0)
424+
# read at least 4 bytes incase we are reading a b64 stream
425+
content = file_handle.read(4)
426+
is_binary = isinstance(content, bytes)
427+
except UnicodeError:
428+
is_binary = False
429+
430+
if not is_binary:
431+
raise ValueError("Files must be opened in binary mode")
432+
433+
file_handle.seek(0, SEEK_END)
434+
content_length = file_handle.tell()
435+
file_handle.seek(0)
436+
return content_length
437+
431438
@staticmethod
432439
def _send(socket: SocketType, data: bytes):
433440
total_sent = 0
@@ -458,13 +465,16 @@ def _send_boundary_objects(self, socket: SocketType, boundary_objects: Any):
458465
if isinstance(boundary_object, str):
459466
self._send_as_bytes(socket, boundary_object)
460467
else:
461-
chunk_size = 32
462-
b = bytearray(chunk_size)
463-
while True:
464-
size = boundary_object.readinto(b)
465-
if size == 0:
466-
break
467-
self._send(socket, b[:size])
468+
self._send_file(socket, boundary_object)
469+
470+
def _send_file(self, socket: SocketType, file_handle: IO):
471+
chunk_size = 36
472+
b = bytearray(chunk_size)
473+
while True:
474+
size = file_handle.readinto(b)
475+
if size == 0:
476+
break
477+
self._send(socket, b[:size])
468478

469479
def _send_header(self, socket, header, value):
470480
if value is None:
@@ -517,12 +527,16 @@ def _send_request( # pylint: disable=too-many-arguments
517527

518528
# If files are send, build data to send and calculate length
519529
content_length = 0
530+
data_is_file = False
520531
boundary_objects = None
521532
if files and isinstance(files, dict):
522533
boundary_string, content_length, boundary_objects = (
523534
self._build_boundary_data(files)
524535
)
525536
content_type_header = f"multipart/form-data; boundary={boundary_string}"
537+
elif data and hasattr(data, "read"):
538+
data_is_file = True
539+
content_length = self._get_file_length(data)
526540
else:
527541
if data is None:
528542
data = b""
@@ -551,7 +565,9 @@ def _send_request( # pylint: disable=too-many-arguments
551565
self._send(socket, b"\r\n")
552566

553567
# Send data
554-
if data:
568+
if data_is_file:
569+
self._send_file(socket, data)
570+
elif data:
555571
self._send(socket, bytes(data))
556572
elif boundary_objects:
557573
self._send_boundary_objects(socket, boundary_objects)

tests/files_test.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def get_actual_request_data(log_stream):
5050
boundary = boundary_search[0]
5151
if content_length_search:
5252
content_length = content_length_search[0]
53-
if "Content-Disposition" in log_arg:
53+
if "Content-Disposition" in log_arg or "\\x" in log_arg:
5454
# this will look like:
5555
# b\'{content}\'
5656
# and escaped characters look like:
@@ -63,6 +63,28 @@ def get_actual_request_data(log_stream):
6363
return boundary, content_length, actual_request_post
6464

6565

66+
def test_post_file_as_data( # pylint: disable=unused-argument
67+
requests, sock, log_stream, post_url, request_logging
68+
):
69+
with open("tests/files/red_green.png", "rb") as file_1:
70+
python_requests.post(post_url, data=file_1, timeout=30)
71+
__, content_length, actual_request_post = get_actual_request_data(log_stream)
72+
73+
requests.post("http://" + mocket.MOCK_HOST_1 + "/post", data=file_1)
74+
75+
sock.connect.assert_called_once_with((mocket.MOCK_POOL_IP, 80))
76+
sock.send.assert_has_calls(
77+
[
78+
mock.call(b"Content-Length"),
79+
mock.call(b": "),
80+
mock.call(content_length.encode()),
81+
mock.call(b"\r\n"),
82+
]
83+
)
84+
sent = b"".join(sock.sent_data)
85+
assert sent.endswith(actual_request_post)
86+
87+
6688
def test_post_files_text( # pylint: disable=unused-argument
6789
sock, requests, log_stream, post_url, request_logging
6890
):

0 commit comments

Comments
 (0)