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,6 +360,60 @@ 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 ):
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
+
369
417
@staticmethod
370
418
def _check_headers (headers : Dict [str , str ]):
371
419
if not isinstance (headers , dict ):
@@ -399,10 +447,31 @@ def _send(socket: SocketType, data: bytes):
399
447
# Not EAGAIN; that was already handled.
400
448
raise OSError (errno .EIO )
401
449
total_sent += sent
450
+ return total_sent
402
451
403
452
def _send_as_bytes (self , socket : SocketType , data : str ):
404
453
return self ._send (socket , bytes (data , "utf-8" ))
405
454
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
+
406
475
def _send_header (self , socket , header , value ):
407
476
if value is None :
408
477
return
@@ -440,6 +509,7 @@ def _send_request( # pylint: disable=too-many-arguments
440
509
441
510
# If data is sent and it's a dict, set content type header and convert to string
442
511
if data and isinstance (data , dict ):
512
+ assert files is None
443
513
content_type_header = "application/x-www-form-urlencoded"
444
514
_post_data = ""
445
515
for k in data :
@@ -451,8 +521,18 @@ def _send_request( # pylint: disable=too-many-arguments
451
521
if data and isinstance (data , str ):
452
522
data = bytes (data , "utf-8" )
453
523
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 )
456
536
457
537
self ._send_as_bytes (socket , method )
458
538
self ._send (socket , b" /" )
@@ -461,60 +541,6 @@ def _send_request( # pylint: disable=too-many-arguments
461
541
462
542
# create lower-case supplied header list
463
543
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
544
519
545
# Send headers
520
546
if not "host" in supplied_headers :
@@ -523,8 +549,8 @@ def _send_request( # pylint: disable=too-many-arguments
523
549
self ._send_header (socket , "User-Agent" , "Adafruit CircuitPython" )
524
550
if content_type_header and not "content-type" in supplied_headers :
525
551
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 ))
528
554
# Iterate over keys to avoid tuple alloc
529
555
for header in headers :
530
556
self ._send_header (socket , header , headers [header ])
@@ -533,6 +559,8 @@ def _send_request( # pylint: disable=too-many-arguments
533
559
# Send data
534
560
if data :
535
561
self ._send (socket , bytes (data ))
562
+ elif boundary_objects :
563
+ self ._send_boundary_objects (socket , boundary_objects )
536
564
537
565
# pylint: disable=too-many-branches, too-many-statements, unused-argument, too-many-arguments, too-many-locals
538
566
def request (
0 commit comments