46
46
47
47
from adafruit_connection_manager import get_connection_manager
48
48
49
+ SEEK_END = 2
50
+
49
51
if not sys .implementation .name == "circuitpython" :
50
52
from types import TracebackType
51
53
from typing import Any , Dict , Optional , Type
@@ -344,14 +346,6 @@ def iter_content(self, chunk_size: int = 1, decode_unicode: bool = False) -> byt
344
346
self .close ()
345
347
346
348
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
-
355
349
class Session :
356
350
"""HTTP session that shares sockets and ssl context."""
357
351
@@ -366,10 +360,74 @@ def __init__(
366
360
self ._session_id = session_id
367
361
self ._last_response = None
368
362
363
+ def _build_boundary_data (self , files : dict ): # pylint: disable=too-many-locals
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_handle = 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 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 AttributeError ("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 )
403
+ boundary_objects .append (file_handle )
404
+ boundary_data = ""
405
+ else :
406
+ boundary_data = file_handle
407
+
408
+ boundary_data += "\r \n "
409
+ content_length += len (boundary_data )
410
+ boundary_objects .append (boundary_data )
411
+
412
+ boundary_data = f"--{ boundary_string } --\r \n "
413
+
414
+ content_length += len (boundary_data )
415
+ boundary_objects .append (boundary_data )
416
+
417
+ return boundary_string , content_length , boundary_objects
418
+
419
+ @staticmethod
420
+ def _build_boundary_string ():
421
+ hex_characters = "0123456789abcdef"
422
+ _boundary = ""
423
+ for _ in range (32 ):
424
+ _boundary += random .choice (hex_characters )
425
+ return _boundary
426
+
369
427
@staticmethod
370
428
def _check_headers (headers : Dict [str , str ]):
371
429
if not isinstance (headers , dict ):
372
- raise AttributeError ("headers must be in dict format" )
430
+ raise AttributeError ("Headers must be in dict format" )
373
431
374
432
for key , value in headers .items ():
375
433
if isinstance (value , (str , bytes )) or value is None :
@@ -403,6 +461,19 @@ def _send(socket: SocketType, data: bytes):
403
461
def _send_as_bytes (self , socket : SocketType , data : str ):
404
462
return self ._send (socket , bytes (data , "utf-8" ))
405
463
464
+ def _send_boundary_objects (self , socket : SocketType , boundary_objects : Any ):
465
+ for boundary_object in boundary_objects :
466
+ if isinstance (boundary_object , str ):
467
+ self ._send_as_bytes (socket , boundary_object )
468
+ else :
469
+ chunk_size = 32
470
+ b = bytearray (chunk_size )
471
+ while True :
472
+ size = boundary_object .readinto (b )
473
+ if size == 0 :
474
+ break
475
+ self ._send (socket , b [:size ])
476
+
406
477
def _send_header (self , socket , header , value ):
407
478
if value is None :
408
479
return
@@ -440,6 +511,7 @@ def _send_request( # pylint: disable=too-many-arguments
440
511
441
512
# If data is sent and it's a dict, set content type header and convert to string
442
513
if data and isinstance (data , dict ):
514
+ assert files is None
443
515
content_type_header = "application/x-www-form-urlencoded"
444
516
_post_data = ""
445
517
for k in data :
@@ -451,8 +523,18 @@ def _send_request( # pylint: disable=too-many-arguments
451
523
if data and isinstance (data , str ):
452
524
data = bytes (data , "utf-8" )
453
525
454
- if data is None :
455
- data = b""
526
+ # If files are send, build data to send and calculate length
527
+ content_length = 0
528
+ boundary_objects = None
529
+ if files and isinstance (files , dict ):
530
+ boundary_string , content_length , boundary_objects = (
531
+ self ._build_boundary_data (files )
532
+ )
533
+ content_type_header = f"multipart/form-data; boundary={ boundary_string } "
534
+ else :
535
+ if data is None :
536
+ data = b""
537
+ content_length = len (data )
456
538
457
539
self ._send_as_bytes (socket , method )
458
540
self ._send (socket , b" /" )
@@ -461,60 +543,6 @@ def _send_request( # pylint: disable=too-many-arguments
461
543
462
544
# create lower-case supplied header list
463
545
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"--"
518
546
519
547
# Send headers
520
548
if not "host" in supplied_headers :
@@ -523,8 +551,8 @@ def _send_request( # pylint: disable=too-many-arguments
523
551
self ._send_header (socket , "User-Agent" , "Adafruit CircuitPython" )
524
552
if content_type_header and not "content-type" in supplied_headers :
525
553
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 ) ))
554
+ if ( data or files ) and not "content-length" in supplied_headers :
555
+ self ._send_header (socket , "Content-Length" , str (content_length ))
528
556
# Iterate over keys to avoid tuple alloc
529
557
for header in headers :
530
558
self ._send_header (socket , header , headers [header ])
@@ -533,6 +561,8 @@ def _send_request( # pylint: disable=too-many-arguments
533
561
# Send data
534
562
if data :
535
563
self ._send (socket , bytes (data ))
564
+ elif boundary_objects :
565
+ self ._send_boundary_objects (socket , boundary_objects )
536
566
537
567
# pylint: disable=too-many-branches, too-many-statements, unused-argument, too-many-arguments, too-many-locals
538
568
def request (
0 commit comments