diff --git a/curl_cffi/__init__.py b/curl_cffi/__init__.py index 263fe113..2d84bbd2 100644 --- a/curl_cffi/__init__.py +++ b/curl_cffi/__init__.py @@ -3,6 +3,7 @@ "CurlInfo", "CurlOpt", "CurlMOpt", + "CurlMime", "CurlECode", "CurlHttpVersion", "CurlError", @@ -16,7 +17,7 @@ from ._wrapper import ffi, lib # type: ignore from .const import CurlInfo, CurlMOpt, CurlOpt, CurlECode, CurlHttpVersion -from .curl import Curl, CurlError +from .curl import Curl, CurlError, CurlMime from .aio import AsyncCurl from .__version__ import __title__, __version__, __description__, __curl_version__ diff --git a/curl_cffi/curl.py b/curl_cffi/curl.py index fb91c105..626bbf42 100644 --- a/curl_cffi/curl.py +++ b/curl_cffi/curl.py @@ -381,6 +381,7 @@ def __init__(self, curl: Optional[Curl] = None): def addpart( self, name: str, + *, type: Optional[str] = None, filename: Optional[str] = None, filepath: Optional[Union[str, bytes, Path]] = None, @@ -418,9 +419,19 @@ def addpart( if fileobj is not None: ret = lib.curl_mime_data(part, fileobj.read()) + @classmethod + def from_list(cls, files: List[dict]): + form = cls() + for file in files: + form.addpart(**file) + return form + def attach(self, curl: Optional[Curl] = None): c = curl if curl else self._curl c.setopt(CurlOpt.MIMEPOST, self._form) def close(self): lib.curl_mime_free(self._form) + + def __del__(self): + self.close() diff --git a/curl_cffi/requests/__init__.py b/curl_cffi/requests/__init__.py index aa95cd0b..cf60e9c3 100644 --- a/curl_cffi/requests/__init__.py +++ b/curl_cffi/requests/__init__.py @@ -25,6 +25,8 @@ from io import BytesIO from typing import Callable, Dict, Optional, Tuple, Union + +from ..curl import CurlMime from ..const import CurlHttpVersion, CurlWsFlag from .cookies import Cookies, CookieTypes from .models import Request, Response @@ -63,6 +65,7 @@ def request( http_version: Optional[CurlHttpVersion] = None, debug: bool = False, interface: Optional[str] = None, + multipart: Optional[CurlMime] = None, ) -> Response: """Send an http request. @@ -122,6 +125,7 @@ def request( default_headers=default_headers, http_version=http_version, interface=interface, + multipart=multipart, ) diff --git a/examples/upload.py b/examples/upload.py new file mode 100644 index 00000000..2444adc7 --- /dev/null +++ b/examples/upload.py @@ -0,0 +1,44 @@ +""" +We do not support requests.post(url, files=...), for 2 reasons. + +- Curl's mime struct need to be freed manually after each request. +- requests' files parameter is quite a mess, it's just not worth it. + +You use the multipart instead, it's very simple and straightforward. +""" +from curl_cffi import requests, CurlMime + + +form = CurlMime() +form.addpart( + "image", # form field name + type="image/png", # mime type + filename="image.png", # filename seen by remote server + filepath="./image.png", # local file to upload +) + +# you can add multiple files under the same field name +form.addpart( + "image", + type="image/jpg", + filename="image.jpg", + fileobj=open("./image.jpg"), # local file to upload, not the difference vs above +) + +# from a list +form = CurlMime.from_list( + [ + { + "name": "image", + "type": "image/png", + "filename": "image.png", + "filepath": "./image.png", + }, + ] +) + +r = requests.get(url, multipart=form) + +# close the form object, otherwise you have to wait for GC to recycle it. If you files +# are too large, you may run out of memory quickly. +form.close()