4141
4242import errno
4343import json as json_module
44+ import os
4445import sys
4546
4647from adafruit_connection_manager import get_connection_manager
4748
49+ SEEK_END = 2
50+
4851if not sys .implementation .name == "circuitpython" :
4952 from types import TracebackType
5053 from typing import Any , Dict , Optional , Type
@@ -357,10 +360,66 @@ def __init__(
357360 self ._session_id = session_id
358361 self ._last_response = None
359362
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+
360419 @staticmethod
361420 def _check_headers (headers : Dict [str , str ]):
362421 if not isinstance (headers , dict ):
363- raise AttributeError ("headers must be in dict format" )
422+ raise AttributeError ("Headers must be in dict format" )
364423
365424 for key , value in headers .items ():
366425 if isinstance (value , (str , bytes )) or value is None :
@@ -394,6 +453,19 @@ def _send(socket: SocketType, data: bytes):
394453 def _send_as_bytes (self , socket : SocketType , data : str ):
395454 return self ._send (socket , bytes (data , "utf-8" ))
396455
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+
397469 def _send_header (self , socket , header , value ):
398470 if value is None :
399471 return
@@ -405,8 +477,7 @@ def _send_header(self, socket, header, value):
405477 self ._send_as_bytes (socket , value )
406478 self ._send (socket , b"\r \n " )
407479
408- # pylint: disable=too-many-arguments
409- def _send_request (
480+ def _send_request ( # pylint: disable=too-many-arguments
410481 self ,
411482 socket : SocketType ,
412483 host : str ,
@@ -415,7 +486,8 @@ def _send_request(
415486 headers : Dict [str , str ],
416487 data : Any ,
417488 json : Any ,
418- ):
489+ files : Optional [Dict [str , tuple ]],
490+ ): # pylint: disable=too-many-branches,too-many-locals,too-many-statements
419491 # Check headers
420492 self ._check_headers (headers )
421493
@@ -425,11 +497,13 @@ def _send_request(
425497 # If json is sent, set content type header and convert to string
426498 if json is not None :
427499 assert data is None
500+ assert files is None
428501 content_type_header = "application/json"
429502 data = json_module .dumps (json )
430503
431504 # If data is sent and it's a dict, set content type header and convert to string
432505 if data and isinstance (data , dict ):
506+ assert files is None
433507 content_type_header = "application/x-www-form-urlencoded"
434508 _post_data = ""
435509 for k in data :
@@ -441,6 +515,19 @@ def _send_request(
441515 if data and isinstance (data , str ):
442516 data = bytes (data , "utf-8" )
443517
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+
444531 self ._send_as_bytes (socket , method )
445532 self ._send (socket , b" /" )
446533 self ._send_as_bytes (socket , path )
@@ -456,8 +543,8 @@ def _send_request(
456543 self ._send_header (socket , "User-Agent" , "Adafruit CircuitPython" )
457544 if content_type_header and not "content-type" in supplied_headers :
458545 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 ))
461548 # Iterate over keys to avoid tuple alloc
462549 for header in headers :
463550 self ._send_header (socket , header , headers [header ])
@@ -466,6 +553,8 @@ def _send_request(
466553 # Send data
467554 if data :
468555 self ._send (socket , bytes (data ))
556+ elif boundary_objects :
557+ self ._send_boundary_objects (socket , boundary_objects )
469558
470559 # pylint: disable=too-many-branches, too-many-statements, unused-argument, too-many-arguments, too-many-locals
471560 def request (
@@ -478,6 +567,7 @@ def request(
478567 stream : bool = False ,
479568 timeout : float = 60 ,
480569 allow_redirects : bool = True ,
570+ files : Optional [Dict [str , tuple ]] = None ,
481571 ) -> Response :
482572 """Perform an HTTP request to the given url which we will parse to determine
483573 whether to use SSL ('https://') or not. We can also send some provided 'data'
@@ -526,7 +616,9 @@ def request(
526616 )
527617 ok = True
528618 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+ )
530622 except OSError as exc :
531623 last_exc = exc
532624 ok = False
0 commit comments