From 0f44b476cf74b708137d26d770a93b10f0387917 Mon Sep 17 00:00:00 2001 From: deedy5 <65482418+deedy5@users.noreply.github.com> Date: Tue, 7 Jan 2025 17:32:44 +0300 Subject: [PATCH] files: take file paths instead of contents; post files as streams --- Cargo.lock | 1 + Cargo.toml | 2 ++ README.md | 6 +++--- src/lib.rs | 33 ++++++++++++++++++++------------- 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 090c5e2..c90078e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1112,6 +1112,7 @@ dependencies = [ "rquest", "serde_json", "tokio", + "tokio-util", "webpki-root-certs", ] diff --git a/Cargo.toml b/Cargo.toml index ce9159e..ef24b14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ rquest = { version = "1.3", features = [ "zstd", "deflate", "multipart", + "stream", "impersonate_str", "impersonate_settings", ] } @@ -32,6 +33,7 @@ encoding_rs = { version = "0.8" } foldhash = "0.1" indexmap = { version = "2", features = ["serde"] } tokio = { version = "1", features = ["full"] } +tokio-util = { version = "0.7", features = ["codec"] } # for multipart html2text = "0.13" bytes = "1" pythonize = "0.23" diff --git a/README.md b/README.md index 674ad66..dde6649 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ def post( content: Optional[bytes] = None, data: Optional[Dict[str, str]] = None, json: Any = None, - files: Optional[Dict[str, bytes]] = None, + files: Optional[Dict[str, str]] = None, auth: Optional[Tuple[str, Optional[str]]] = None, auth_bearer: Optional[str] = None, timeout: Optional[float] = 30, @@ -122,7 +122,7 @@ def post( content (Optional[bytes]): The content to send in the request body as bytes. Default is None. data (Optional[Dict[str, str]]): The form data to send in the request body. Default is None. json (Any): A JSON serializable object to send in the request body. Default is None. - files (Optional[Dict[str, bytes]]): A map of file fields to file contents to be sent as multipart/form-data. Default is None. + files (Optional[Dict[str, str]]): A map of file fields to file paths to be sent as multipart/form-data. Default is None. auth (Optional[Tuple[str, Optional[str]]]): A tuple containing the username and an optional password for basic authentication. Default is None. auth_bearer (Optional[str]): A string representing the bearer token for bearer token authentication. Default is None. @@ -190,7 +190,7 @@ resp = client.post(url="https://httpbin.org/anything", json=json) print(r.text) # POST Multipart-Encoded Files -files = {'file1': open('file1.txt'), 'file2': open('file2.txt')} +files = {'file1': '/home/root/file1.txt', 'file2': 'home/root/file2.txt'} r = client.post("https://httpbin.org/post", files=files) print(r.text) diff --git a/src/lib.rs b/src/lib.rs index 7f6dab9..338cd8e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,10 +15,14 @@ use rquest::{ multipart, redirect::Policy, tls::Impersonate, - Method, + Body, Method, }; use serde_json::Value; -use tokio::runtime::{self, Runtime}; +use tokio::{ + fs::File, + runtime::{self, Runtime}, +}; +use tokio_util::codec::{BytesCodec, FramedRead}; mod response; use response::Response; @@ -289,7 +293,7 @@ impl Client { /// * `content` - The content to send in the request body as bytes. Default is None. /// * `data` - The form data to send in the request body. Default is None. /// * `json` - A JSON serializable object to send in the request body. Default is None. - /// * `files` - A map of file fields to file contents as bytes to be sent as multipart/form-data. Default is None. + /// * `files` - A map of file fields to file paths to be sent as multipart/form-data. Default is None. /// * `auth` - A tuple containing the username and an optional password for basic authentication. Default is None. /// * `auth_bearer` - A string representing the bearer token for bearer token authentication. Default is None. /// * `timeout` - The timeout for the request in seconds. Default is 30. @@ -314,7 +318,7 @@ impl Client { content: Option>, data: Option<&Bound<'_, PyAny>>, json: Option<&Bound<'_, PyAny>>, - files: Option>>, + files: Option>, auth: Option<(String, Option)>, auth_bearer: Option, timeout: Option, @@ -366,8 +370,11 @@ impl Client { // Files if let Some(files) = files { let mut form = multipart::Form::new(); - for (file_name, file_buf) in files { - let part = multipart::Part::stream(file_buf).file_name(file_name.clone()); + for (file_name, file_path) in files { + let file = File::open(file_path).await?; + let stream = FramedRead::new(file, BytesCodec::new()); + let file_body = Body::wrap_stream(stream); + let part = multipart::Part::stream(file_body).file_name(file_name.clone()); form = form.part(file_name, part); } request_builder = request_builder.multipart(form); @@ -555,7 +562,7 @@ impl Client { content: Option>, data: Option<&Bound<'_, PyAny>>, json: Option<&Bound<'_, PyAny>>, - files: Option>>, + files: Option>, auth: Option<(String, Option)>, auth_bearer: Option, timeout: Option, @@ -589,7 +596,7 @@ impl Client { content: Option>, data: Option<&Bound<'_, PyAny>>, json: Option<&Bound<'_, PyAny>>, - files: Option>>, + files: Option>, auth: Option<(String, Option)>, auth_bearer: Option, timeout: Option, @@ -623,7 +630,7 @@ impl Client { content: Option>, data: Option<&Bound<'_, PyAny>>, json: Option<&Bound<'_, PyAny>>, - files: Option>>, + files: Option>, auth: Option<(String, Option)>, auth_bearer: Option, timeout: Option, @@ -661,7 +668,7 @@ fn request( content: Option>, data: Option<&Bound<'_, PyAny>>, json: Option<&Bound<'_, PyAny>>, - files: Option>>, + files: Option>, auth: Option<(String, Option)>, auth_bearer: Option, timeout: Option, @@ -901,7 +908,7 @@ fn post( content: Option>, data: Option<&Bound<'_, PyAny>>, json: Option<&Bound<'_, PyAny>>, - files: Option>>, + files: Option>, auth: Option<(String, Option)>, auth_bearer: Option, timeout: Option, @@ -956,7 +963,7 @@ fn put( content: Option>, data: Option<&Bound<'_, PyAny>>, json: Option<&Bound<'_, PyAny>>, - files: Option>>, + files: Option>, auth: Option<(String, Option)>, auth_bearer: Option, timeout: Option, @@ -1011,7 +1018,7 @@ fn patch( content: Option>, data: Option<&Bound<'_, PyAny>>, json: Option<&Bound<'_, PyAny>>, - files: Option>>, + files: Option>, auth: Option<(String, Option)>, auth_bearer: Option, timeout: Option,