4646
4747from adafruit_connection_manager import get_connection_manager
4848
49+ SEEK_END = 2
50+
4951if not sys .implementation .name == "circuitpython" :
5052 from types import TracebackType
5153 from typing import Any , Dict , Optional , Type
@@ -344,14 +346,6 @@ def iter_content(self, chunk_size: int = 1, decode_unicode: bool = False) -> byt
344346 self .close ()
345347
346348
347- def _generate_boundary_str ():
348- hex_characters = "0123456789abcdef"
349- _boundary = ""
350- for _ in range (32 ):
351- _boundary += random .choice (hex_characters )
352- return _boundary
353-
354-
355349class Session :
356350 """HTTP session that shares sockets and ssl context."""
357351
@@ -366,6 +360,60 @@ def __init__(
366360 self ._session_id = session_id
367361 self ._last_response = None
368362
363+ def _build_boundary_data (self , files : dict ):
364+ boundary_string = self ._build_boundary_string ()
365+ content_length = 0
366+ boundary_objects = []
367+
368+ for field_name , field_values in files .items ():
369+ file_name = field_values [0 ]
370+ file_data = field_values [1 ]
371+
372+ boundary_data = f"--{ boundary_string } \r \n "
373+ boundary_data += f'Content-Disposition: form-data; name="{ field_name } "; '
374+ if file_name is not None :
375+ boundary_data += f'filename="{ file_name } "'
376+ boundary_data += "\r \n "
377+ if len (field_values ) >= 3 :
378+ file_content_type = field_values [2 ]
379+ boundary_data += f"Content-Type: { file_content_type } \r \n "
380+ if len (field_values ) >= 4 :
381+ file_headers = field_values [3 ]
382+ for file_header_key , file_header_value in file_headers .items ():
383+ boundary_data += f"{ file_header_key } : { file_header_value } \r \n "
384+ boundary_data += "\r \n "
385+
386+ content_length += len (boundary_data )
387+ boundary_objects .append (boundary_data )
388+
389+ if file_name is not None :
390+ file_data .seek (0 , SEEK_END )
391+ content_length += file_data .tell ()
392+ file_data .seek (0 )
393+ boundary_objects .append (file_data )
394+ boundary_data = ""
395+ else :
396+ boundary_data = file_data
397+
398+ boundary_data += "\r \n "
399+ content_length += len (boundary_data )
400+ boundary_objects .append (boundary_data )
401+
402+ boundary_data = f"--{ boundary_string } --"
403+
404+ content_length += len (boundary_data )
405+ boundary_objects .append (boundary_data )
406+
407+ return boundary_string , content_length , boundary_objects
408+
409+ @staticmethod
410+ def _build_boundary_string ():
411+ hex_characters = "0123456789abcdef"
412+ _boundary = ""
413+ for _ in range (32 ):
414+ _boundary += random .choice (hex_characters )
415+ return _boundary
416+
369417 @staticmethod
370418 def _check_headers (headers : Dict [str , str ]):
371419 if not isinstance (headers , dict ):
@@ -399,10 +447,31 @@ def _send(socket: SocketType, data: bytes):
399447 # Not EAGAIN; that was already handled.
400448 raise OSError (errno .EIO )
401449 total_sent += sent
450+ return total_sent
402451
403452 def _send_as_bytes (self , socket : SocketType , data : str ):
404453 return self ._send (socket , bytes (data , "utf-8" ))
405454
455+ def _send_boundary_objects (self , socket : SocketType , boundary_objects : Any ):
456+ for boundary_object in boundary_objects :
457+ if isinstance (boundary_object , str ):
458+ self ._send_as_bytes (socket , boundary_object )
459+ else :
460+ chunk_size = 32
461+ if hasattr (boundary_object , "readinto" ):
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+ else :
469+ while True :
470+ b = boundary_object .read (chunk_size )
471+ if len (b ) == 0 :
472+ break
473+ self ._send (socket , b )
474+
406475 def _send_header (self , socket , header , value ):
407476 if value is None :
408477 return
@@ -440,6 +509,7 @@ def _send_request( # pylint: disable=too-many-arguments
440509
441510 # If data is sent and it's a dict, set content type header and convert to string
442511 if data and isinstance (data , dict ):
512+ assert files is None
443513 content_type_header = "application/x-www-form-urlencoded"
444514 _post_data = ""
445515 for k in data :
@@ -451,8 +521,18 @@ def _send_request( # pylint: disable=too-many-arguments
451521 if data and isinstance (data , str ):
452522 data = bytes (data , "utf-8" )
453523
454- if data is None :
455- data = b""
524+ # If files are send, build data to send and calculate length
525+ content_length = 0
526+ boundary_objects = None
527+ if files and isinstance (files , dict ):
528+ boundary_string , content_length , boundary_objects = (
529+ self ._build_boundary_data (files )
530+ )
531+ content_type_header = f"multipart/form-data; boundary={ boundary_string } "
532+ else :
533+ if data is None :
534+ data = b""
535+ content_length = len (data )
456536
457537 self ._send_as_bytes (socket , method )
458538 self ._send (socket , b" /" )
@@ -461,60 +541,6 @@ def _send_request( # pylint: disable=too-many-arguments
461541
462542 # create lower-case supplied header list
463543 supplied_headers = {header .lower () for header in headers }
464- boundary_str = None
465-
466- # pylint: disable=too-many-nested-blocks
467- if files is not None and isinstance (files , dict ):
468- boundary_str = _generate_boundary_str ()
469- content_type_header = f"multipart/form-data; boundary={ boundary_str } "
470-
471- for fieldname in files .keys ():
472- if not fieldname .endswith ("-name" ):
473- if files [fieldname ][0 ] is not None :
474- file_content = files [fieldname ][1 ].read ()
475-
476- data += b"--" + boundary_str .encode () + b"\r \n "
477- data += (
478- b'Content-Disposition: form-data; name="'
479- + fieldname .encode ()
480- + b'"; filename="'
481- + files [fieldname ][0 ].encode ()
482- + b'"\r \n '
483- )
484- if len (files [fieldname ]) >= 3 :
485- data += (
486- b"Content-Type: "
487- + files [fieldname ][2 ].encode ()
488- + b"\r \n "
489- )
490- if len (files [fieldname ]) >= 4 :
491- for custom_header_key in files [fieldname ][3 ].keys ():
492- data += (
493- custom_header_key .encode ()
494- + b": "
495- + files [fieldname ][3 ][custom_header_key ].encode ()
496- + b"\r \n "
497- )
498- data += b"\r \n "
499- data += file_content + b"\r \n "
500- else :
501- # filename is None
502- data += b"--" + boundary_str .encode () + b"\r \n "
503- data += (
504- b'Content-Disposition: form-data; name="'
505- + fieldname .encode ()
506- + b'"; \r \n '
507- )
508- if len (files [fieldname ]) >= 3 :
509- data += (
510- b"Content-Type: "
511- + files [fieldname ][2 ].encode ()
512- + b"\r \n "
513- )
514- data += b"\r \n "
515- data += files [fieldname ][1 ].encode () + b"\r \n "
516-
517- data += b"--" + boundary_str .encode () + b"--"
518544
519545 # Send headers
520546 if not "host" in supplied_headers :
@@ -523,8 +549,8 @@ def _send_request( # pylint: disable=too-many-arguments
523549 self ._send_header (socket , "User-Agent" , "Adafruit CircuitPython" )
524550 if content_type_header and not "content-type" in supplied_headers :
525551 self ._send_header (socket , "Content-Type" , content_type_header )
526- if data and not "content-length" in supplied_headers :
527- self ._send_header (socket , "Content-Length" , str (len ( data ) ))
552+ if ( data or files ) and not "content-length" in supplied_headers :
553+ self ._send_header (socket , "Content-Length" , str (content_length ))
528554 # Iterate over keys to avoid tuple alloc
529555 for header in headers :
530556 self ._send_header (socket , header , headers [header ])
@@ -533,6 +559,8 @@ def _send_request( # pylint: disable=too-many-arguments
533559 # Send data
534560 if data :
535561 self ._send (socket , bytes (data ))
562+ elif boundary_objects :
563+ self ._send_boundary_objects (socket , boundary_objects )
536564
537565 # pylint: disable=too-many-branches, too-many-statements, unused-argument, too-many-arguments, too-many-locals
538566 def request (
0 commit comments