41
41
42
42
import errno
43
43
import json as json_module
44
+ import os
44
45
import sys
45
46
46
47
from adafruit_connection_manager import get_connection_manager
47
48
49
+ SEEK_END = 2
50
+
48
51
if not sys .implementation .name == "circuitpython" :
49
52
from types import TracebackType
50
53
from typing import Any , Dict , Optional , Type
@@ -357,10 +360,66 @@ def __init__(
357
360
self ._session_id = session_id
358
361
self ._last_response = None
359
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_objects .append (
373
+ f'--{ boundary_string } \r \n Content-Disposition: form-data; name="{ field_name } "'
374
+ )
375
+ if file_name is not None :
376
+ boundary_objects .append (f'; filename="{ file_name } "' )
377
+ boundary_objects .append ("\r \n " )
378
+ if len (field_values ) >= 3 :
379
+ file_content_type = field_values [2 ]
380
+ boundary_objects .append (f"Content-Type: { file_content_type } \r \n " )
381
+ if len (field_values ) >= 4 :
382
+ file_headers = field_values [3 ]
383
+ for file_header_key , file_header_value in file_headers .items ():
384
+ boundary_objects .append (
385
+ f"{ file_header_key } : { file_header_value } \r \n "
386
+ )
387
+ boundary_objects .append ("\r \n " )
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
+
404
+ boundary_objects .append (file_handle )
405
+ boundary_objects .append ("\r \n " )
406
+
407
+ boundary_objects .append (f"--{ boundary_string } --\r \n " )
408
+
409
+ for boundary_object in boundary_objects :
410
+ if isinstance (boundary_object , str ):
411
+ content_length += len (boundary_object )
412
+
413
+ return boundary_string , content_length , boundary_objects
414
+
415
+ @staticmethod
416
+ def _build_boundary_string ():
417
+ return os .urandom (16 ).hex ()
418
+
360
419
@staticmethod
361
420
def _check_headers (headers : Dict [str , str ]):
362
421
if not isinstance (headers , dict ):
363
- raise AttributeError ("headers must be in dict format" )
422
+ raise AttributeError ("Headers must be in dict format" )
364
423
365
424
for key , value in headers .items ():
366
425
if isinstance (value , (str , bytes )) or value is None :
@@ -394,6 +453,19 @@ def _send(socket: SocketType, data: bytes):
394
453
def _send_as_bytes (self , socket : SocketType , data : str ):
395
454
return self ._send (socket , bytes (data , "utf-8" ))
396
455
456
+ def _send_boundary_objects (self , socket : SocketType , boundary_objects : Any ):
457
+ for boundary_object in boundary_objects :
458
+ if isinstance (boundary_object , str ):
459
+ self ._send_as_bytes (socket , boundary_object )
460
+ 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
+
397
469
def _send_header (self , socket , header , value ):
398
470
if value is None :
399
471
return
@@ -405,8 +477,7 @@ def _send_header(self, socket, header, value):
405
477
self ._send_as_bytes (socket , value )
406
478
self ._send (socket , b"\r \n " )
407
479
408
- # pylint: disable=too-many-arguments
409
- def _send_request (
480
+ def _send_request ( # pylint: disable=too-many-arguments
410
481
self ,
411
482
socket : SocketType ,
412
483
host : str ,
@@ -415,7 +486,8 @@ def _send_request(
415
486
headers : Dict [str , str ],
416
487
data : Any ,
417
488
json : Any ,
418
- ):
489
+ files : Optional [Dict [str , tuple ]],
490
+ ): # pylint: disable=too-many-branches,too-many-locals,too-many-statements
419
491
# Check headers
420
492
self ._check_headers (headers )
421
493
@@ -425,11 +497,13 @@ def _send_request(
425
497
# If json is sent, set content type header and convert to string
426
498
if json is not None :
427
499
assert data is None
500
+ assert files is None
428
501
content_type_header = "application/json"
429
502
data = json_module .dumps (json )
430
503
431
504
# If data is sent and it's a dict, set content type header and convert to string
432
505
if data and isinstance (data , dict ):
506
+ assert files is None
433
507
content_type_header = "application/x-www-form-urlencoded"
434
508
_post_data = ""
435
509
for k in data :
@@ -441,6 +515,19 @@ def _send_request(
441
515
if data and isinstance (data , str ):
442
516
data = bytes (data , "utf-8" )
443
517
518
+ # If files are send, build data to send and calculate length
519
+ content_length = 0
520
+ boundary_objects = None
521
+ if files and isinstance (files , dict ):
522
+ boundary_string , content_length , boundary_objects = (
523
+ self ._build_boundary_data (files )
524
+ )
525
+ content_type_header = f"multipart/form-data; boundary={ boundary_string } "
526
+ else :
527
+ if data is None :
528
+ data = b""
529
+ content_length = len (data )
530
+
444
531
self ._send_as_bytes (socket , method )
445
532
self ._send (socket , b" /" )
446
533
self ._send_as_bytes (socket , path )
@@ -456,8 +543,8 @@ def _send_request(
456
543
self ._send_header (socket , "User-Agent" , "Adafruit CircuitPython" )
457
544
if content_type_header and not "content-type" in supplied_headers :
458
545
self ._send_header (socket , "Content-Type" , content_type_header )
459
- if data and not "content-length" in supplied_headers :
460
- self ._send_header (socket , "Content-Length" , str (len ( data ) ))
546
+ if ( data or files ) and not "content-length" in supplied_headers :
547
+ self ._send_header (socket , "Content-Length" , str (content_length ))
461
548
# Iterate over keys to avoid tuple alloc
462
549
for header in headers :
463
550
self ._send_header (socket , header , headers [header ])
@@ -466,6 +553,8 @@ def _send_request(
466
553
# Send data
467
554
if data :
468
555
self ._send (socket , bytes (data ))
556
+ elif boundary_objects :
557
+ self ._send_boundary_objects (socket , boundary_objects )
469
558
470
559
# pylint: disable=too-many-branches, too-many-statements, unused-argument, too-many-arguments, too-many-locals
471
560
def request (
@@ -478,6 +567,7 @@ def request(
478
567
stream : bool = False ,
479
568
timeout : float = 60 ,
480
569
allow_redirects : bool = True ,
570
+ files : Optional [Dict [str , tuple ]] = None ,
481
571
) -> Response :
482
572
"""Perform an HTTP request to the given url which we will parse to determine
483
573
whether to use SSL ('https://') or not. We can also send some provided 'data'
@@ -526,7 +616,9 @@ def request(
526
616
)
527
617
ok = True
528
618
try :
529
- self ._send_request (socket , host , method , path , headers , data , json )
619
+ self ._send_request (
620
+ socket , host , method , path , headers , data , json , files
621
+ )
530
622
except OSError as exc :
531
623
last_exc = exc
532
624
ok = False
0 commit comments