diff --git a/Cargo.lock b/Cargo.lock index bd3f57ed0..1e2dccf7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ checksum = "f728064aca1c318585bf4bb04ffcfac9e75e508ab4e8b1bd9ba5dfe04e2cbed5" dependencies = [ "actix-rt", "actix_derive", - "bitflags", + "bitflags 1.3.2", "bytes", "crossbeam-channel", "futures-core", @@ -32,7 +32,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" dependencies = [ - "bitflags", + "bitflags 1.3.2", "bytes", "futures-core", "futures-sink", @@ -54,7 +54,7 @@ dependencies = [ "actix-utils", "actix-web", "askama_escape", - "bitflags", + "bitflags 1.3.2", "bytes", "derive_more", "futures-core", @@ -78,7 +78,7 @@ dependencies = [ "actix-utils", "ahash 0.8.3", "base64", - "bitflags", + "bitflags 1.3.2", "brotli", "bytes", "bytestring", @@ -115,6 +115,44 @@ dependencies = [ "syn 2.0.27", ] +[[package]] +name = "actix-multipart" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b960e2aea75f49c8f069108063d12a48d329fc8b60b786dfc7552a9d5918d2d" +dependencies = [ + "actix-multipart-derive", + "actix-utils", + "actix-web", + "bytes", + "derive_more", + "futures-core", + "futures-util", + "httparse", + "local-waker", + "log", + "memchr", + "mime", + "serde", + "serde_json", + "serde_plain", + "tempfile", + "tokio", +] + +[[package]] +name = "actix-multipart-derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a0a77f836d869f700e5b47ac7c3c8b9c8bc82e4aec861954c6198abee3ebd4d" +dependencies = [ + "darling", + "parse-size", + "proc-macro2", + "quote", + "syn 2.0.27", +] + [[package]] name = "actix-router" version = "0.5.1" @@ -374,6 +412,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + [[package]] name = "block-buffer" version = "0.10.4" @@ -498,6 +542,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.27", +] + +[[package]] +name = "darling_macro" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.27", +] + [[package]] name = "dashmap" version = "5.5.0" @@ -543,6 +622,22 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" + [[package]] name = "flate2" version = "1.0.26" @@ -750,6 +845,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.4.0" @@ -782,7 +883,7 @@ version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd1e1a01cfb924fd8c5c43b6827965db394f5a3a16c599ce03452266e1cf984c" dependencies = [ - "bitflags", + "bitflags 1.3.2", "libc", ] @@ -809,9 +910,15 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "local-channel" @@ -902,7 +1009,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -950,9 +1057,15 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.1", ] +[[package]] +name = "parse-size" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae" + [[package]] name = "paste" version = "1.0.14" @@ -1149,7 +1262,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -1188,6 +1301,7 @@ dependencies = [ "actix", "actix-files", "actix-http", + "actix-multipart", "actix-web", "actix-web-actors", "anyhow", @@ -1223,6 +1337,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "ryu" version = "1.0.15" @@ -1264,6 +1391,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1328,9 +1464,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.109" @@ -1359,6 +1501,18 @@ version = "0.12.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d2faeef5759ab89935255b1a4cd98e0baf99d1085e37d36599c625dac49ae8e" +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + [[package]] name = "time" version = "0.3.23" @@ -1418,7 +1572,7 @@ dependencies = [ "signal-hook-registry", "socket2 0.4.9", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1584,7 +1738,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.1", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", ] [[package]] @@ -1593,13 +1756,28 @@ version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] [[package]] @@ -1608,42 +1786,84 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + [[package]] name = "windows_aarch64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + [[package]] name = "windows_i686_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + [[package]] name = "windows_i686_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + [[package]] name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + [[package]] name = "zstd" version = "0.12.4" diff --git a/Cargo.toml b/Cargo.toml index 2078ccc05..fa81bf9d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ pythonize = "0.19.0" serde = "1.0.178" serde_json = "1.0.104" once_cell = "1.8.0" +actix-multipart = "0.6.1" [features] io-uring = ["actix-web/experimental-io-uring"] diff --git a/README.md b/README.md index 5be052088..9bce73def 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ python --version - Multi Core Scaling - WebSockets! - Middlewares +- Built in form data handling - Dependency Injection - Hot Reloading - Direct Rust Integration diff --git a/docs_src/src/components/documentation/ApiDocs.jsx b/docs_src/src/components/documentation/ApiDocs.jsx index 008f255d0..cb055bee9 100644 --- a/docs_src/src/components/documentation/ApiDocs.jsx +++ b/docs_src/src/components/documentation/ApiDocs.jsx @@ -53,6 +53,11 @@ const guides = [ description: 'Learn how to upload and download files to your server using Robyn.', }, + { + href: '/documentation/api_reference/form_data', + name: 'Form Data', + description: 'Learn how to handle form data.', + }, { href: '/documentation/api_reference/websockets', name: 'Websockets', diff --git a/docs_src/src/components/documentation/Navigation.jsx b/docs_src/src/components/documentation/Navigation.jsx index 0e27227b1..8805197c5 100644 --- a/docs_src/src/components/documentation/Navigation.jsx +++ b/docs_src/src/components/documentation/Navigation.jsx @@ -260,6 +260,10 @@ export const navigation = [ href: '/documentation/api_reference/file-uploads', title: 'File Uploads', }, + { + href: '/documentation/api_reference/form_data', + title: 'Form Data', + }, { href: '/documentation/api_reference/websockets', title: 'Websockets', diff --git a/docs_src/src/pages/documentation/api_reference/file-uploads.mdx b/docs_src/src/pages/documentation/api_reference/file-uploads.mdx index 2fdd0265b..dec4aa926 100644 --- a/docs_src/src/pages/documentation/api_reference/file-uploads.mdx +++ b/docs_src/src/pages/documentation/api_reference/file-uploads.mdx @@ -6,7 +6,7 @@ export const description = Batman learned how to handle file uploads using Robyn. He created an endpoint to handle file uploads using the following code: -## Scaling the Application +## Sending a File without MultiPart Form Data @@ -17,8 +17,6 @@ Batman scaled his application across multiple cores for better performance. He u ```python {{ title: 'untyped' }} - from robyn.robyn import File - @app.post("/upload") async def upload(): body = request.body @@ -31,8 +29,6 @@ Batman scaled his application across multiple cores for better performance. He u return {'message': 'success'} ``` ```python {{ title: 'typed' }} - from robyn.robyn import File - @app.post("/upload") async def upload(): body = request.body @@ -49,6 +45,44 @@ Batman scaled his application across multiple cores for better performance. He u + +## Sending a File with MultiPart Form Data + + + +Batman scaled his application across multiple cores for better performance. He used the following command: + + + + + + ```python {{ title: 'untyped' }} + @app.post("/upload") + async def upload(request): + file = request.files['filename'] + + # write whatever filename + with open('filename', 'wb') as f: + f.write(file) + + return {'message': 'success'} + ``` + ```python {{ title: 'typed' }} + @app.post("/upload") + async def upload(request: Request): + file = request.files['filename'] + + # write whatever filename + with open('filename', 'wb') as f: + f.write(file) + + return {'message': 'success'} + ``` + + + + + --- ## File Downloads @@ -186,9 +220,9 @@ Batman scaled his application across multiple cores for better performance. He u ## What's next? -Now, Batman was ready to learn about the advanced features of Robyn. He wanted to find a way to get realtime updates in his dashboard. +Now, Batman was ready to learn about the advanced features of Robyn. He wanted to find a way to handle form data -- [WebSockets](/documentation/api_reference/websockets) +- [Form Data](/documentation/api_reference/form_data) diff --git a/docs_src/src/pages/documentation/api_reference/form_data.mdx b/docs_src/src/pages/documentation/api_reference/form_data.mdx new file mode 100644 index 000000000..fe06179bf --- /dev/null +++ b/docs_src/src/pages/documentation/api_reference/form_data.mdx @@ -0,0 +1,43 @@ +export const description = + 'On this page, we’ll dive into using the form data.' + +## Fom Data + +Batman learned how to handle file uploads using Robyn. Now, he wanted to handle the form data + + +## Handling Form Data + + + +Batman uploaded some multipart form data and wanted to handle it using the following code: + + + + + + ```python {{ title: 'untyped' }} + @app.post("/upload") + async def upload(request): + form_data = request.from_data + + return form_data + ``` + ```python {{ title: 'typed' }} + @app.post("/upload") + async def upload(request): + form_data = request.from_data + + return form_data + ``` + + + + + + +## What's next? + +Now, Batman was ready to learn about the advanced features of Robyn. He wanted to find a way to get realtime updates in his dashboard. + +- [WebSockets](/documentation/api_reference/websockets) diff --git a/integration_tests/base_routes.py b/integration_tests/base_routes.py index 9bc841ed2..b87ed139d 100644 --- a/integration_tests/base_routes.py +++ b/integration_tests/base_routes.py @@ -460,6 +460,17 @@ async def file_download_async(): return serve_file(file_path) +# Multipart file + + +@app.post("/sync/multipart-file") +def sync_multipart_file(request: Request): + file = request.files["test.png"] + with open("test.png", "wb") as f: + f.write(file) + return "File uploaded" + + # Queries @@ -548,6 +559,11 @@ async def async_body_post(request: Request): return request.body +@app.post("/sync/form_data") +def sync_form_data(request: Request): + return request.form_data + + # JSON Request @@ -631,6 +647,7 @@ async def async_dict_delete(): @app.delete("/sync/body") def sync_body_delete(request: Request): + print(request.body) return request.body diff --git a/integration_tests/helpers/http_methods_helpers.py b/integration_tests/helpers/http_methods_helpers.py index 921375e72..b78fecfaa 100644 --- a/integration_tests/helpers/http_methods_helpers.py +++ b/integration_tests/helpers/http_methods_helpers.py @@ -11,10 +11,10 @@ def check_response(response: requests.Response, expected_status_code: int): headers is not present in the response. """ assert response.status_code == expected_status_code - print(response.headers) assert response.headers.get("global_after") == "global_after_request" assert "server" in response.headers assert response.headers.get("server") == "robyn" + print("This is the response text", response) def get( diff --git a/robyn/robyn.pyi b/robyn/robyn.pyi index d5b7b916d..383adf8dd 100644 --- a/robyn/robyn.pyi +++ b/robyn/robyn.pyi @@ -246,10 +246,13 @@ class Request: Attributes: query_params (QueryParams): The query parameters of the request. e.g. /user?id=123 -> {"id": "123"} headers Headers: The headers of the request. e.g. Headers({"Content-Type": "application/json"}) - params (dict[str, str]): The parameters of the request. e.g. /user/:id -> {"id": "123"} + path_params (dict[str, str]): The parameters of the request. e.g. /user/:id -> {"id": "123"} body (Union[str, bytes]): The body of the request. If the request is a JSON, it will be a dict. - method (str): The method of the request. e.g. GET, POST, PUT, DELETE + url (Url): The url of the request. e.g. https://localhost/user + form_data (dict[str, str]): The form data of the request. e.g. {"name": "John"} + files (dict[str, bytes]): The files of the request. e.g. {"file": b"file"} ip_addr (Optional[str]): The IP Address of the client + identity (Optional[Identity]): The identity of the client """ query_params: QueryParams @@ -258,6 +261,8 @@ class Request: body: Union[str, bytes] method: str url: Url + form_data: dict[str, str] + files: dict[str, bytes] ip_addr: Optional[str] identity: Optional[Identity] diff --git a/src/server.rs b/src/server.rs index bd52db435..28f49ec25 100644 --- a/src/server.rs +++ b/src/server.rs @@ -23,7 +23,6 @@ use std::{env, thread}; use actix_files::Files; use actix_http::KeepAlive; -use actix_web::web::Bytes; use actix_web::*; // pyO3 module @@ -190,18 +189,18 @@ impl Server { move |router: web::Data>, const_router: web::Data>, middleware_router: web::Data>, + payload: web::Payload, global_request_headers, global_response_headers, - body, req| { pyo3_asyncio::tokio::scope_local(task_locals.clone(), async move { index( router, + payload, const_router, middleware_router, global_request_headers, global_response_headers, - body, req, ) .await @@ -366,14 +365,14 @@ impl Default for Server { /// path, and returns a Future of a Response. async fn index( router: web::Data>, + payload: web::Payload, const_router: web::Data>, middleware_router: web::Data>, global_request_headers: web::Data>, global_response_headers: web::Data>, - body: Bytes, req: HttpRequest, ) -> impl Responder { - let mut request = Request::from_actix_request(&req, body, &global_request_headers); + let mut request = Request::from_actix_request(&req, payload, &global_request_headers).await; // Before middleware // Global diff --git a/src/types/headers.rs b/src/types/headers.rs index a388d08bc..129477af1 100644 --- a/src/types/headers.rs +++ b/src/types/headers.rs @@ -23,13 +23,12 @@ impl Headers { let new_value = value.downcast::(); - if new_value.is_err() { + if let Ok(new_value) = new_value { + let value: Vec = new_value.iter().map(|x| x.to_string()).collect(); + headers.entry(key).or_insert_with(Vec::new).extend(value); + } else { let value = value.to_string(); headers.entry(key).or_insert_with(Vec::new).push(value); - } else { - let value: Vec = - new_value.unwrap().iter().map(|x| x.to_string()).collect(); - headers.entry(key).or_insert_with(Vec::new).extend(value); } } Headers { headers } @@ -100,12 +99,12 @@ impl Headers { let key = key.to_string().to_lowercase(); let new_value = value.downcast::(); - if new_value.is_err() { + if let Ok(new_value) = new_value { + let value: Vec = new_value.iter().map(|x| x.to_string()).collect(); + self.headers.entry(key).or_default().extend(value); + } else { let value = value.to_string(); self.headers.entry(key).or_default().push(value); - } else { - let value: Vec = new_value.unwrap().iter().map(|x| x.to_string()).collect(); - self.headers.entry(key).or_default().extend(value); } } } diff --git a/src/types/request.rs b/src/types/request.rs index 98ed788d6..78b2cffcf 100644 --- a/src/types/request.rs +++ b/src/types/request.rs @@ -1,5 +1,12 @@ -use actix_web::{web::Bytes, HttpRequest}; -use pyo3::{exceptions::PyValueError, prelude::*, types::PyDict, types::PyString}; +use actix_multipart::Multipart; +use actix_web::{ + web::{self, BytesMut}, + Error, HttpRequest, +}; +use futures_util::StreamExt as _; +use log::debug; +use pyo3::types::{PyBytes, PyDict, PyString}; +use pyo3::{exceptions::PyValueError, prelude::*}; use serde_json::Value; use std::collections::HashMap; @@ -19,6 +26,8 @@ pub struct Request { pub url: Url, pub ip_addr: Option, pub identity: Option, + pub form_data: Option>, + pub files: Option>>, } impl ToPyObject for Request { @@ -30,6 +39,28 @@ impl ToPyObject for Request { Ok(s) => s.into_py(py), Err(_) => self.body.clone().into_py(py), }; + let form_data: Py = match &self.form_data { + Some(data) => { + let dict = PyDict::new(py); + for (key, value) in data.iter() { + dict.set_item(key, value).unwrap(); + } + dict.into_py(py) + } + None => PyDict::new(py).into_py(py), + }; + + let files: Py = match &self.files { + Some(data) => { + let dict = PyDict::new(py); + for (key, value) in data.iter() { + let bytes = PyBytes::new(py, value); + dict.set_item(key, bytes).unwrap(); + } + dict.into_py(py) + } + None => PyDict::new(py).into_py(py), + }; let request = PyRequest { query_params, @@ -40,14 +71,58 @@ impl ToPyObject for Request { url: self.url.clone(), ip_addr: self.ip_addr.clone(), identity: self.identity.clone(), + form_data: form_data.clone(), + files: files.clone(), }; Py::new(py, request).unwrap().as_ref(py).into() } } +async fn handle_multipart( + mut payload: Multipart, + files: &mut HashMap>, + form_data: &mut HashMap, + body: &mut Vec, +) -> Result<(), Error> { + // Iterate over multipart stream + + while let Some(item) = payload.next().await { + let mut field = item?; + + let mut data = Vec::new(); + // Read the field data + while let Some(chunk) = field.next().await { + debug!("Chunk: {:?}", chunk); + let data_chunk = chunk?; + data.extend_from_slice(&data_chunk); + } + + let content_disposition = field.content_disposition(); + let field_name = content_disposition.get_name().unwrap_or_default(); + let file_name = content_disposition.get_filename().map(|s| s.to_string()); + + body.extend_from_slice(&data.clone()); + + if let Some(name) = file_name { + files.insert(name, data); + } else if let Ok(text) = String::from_utf8(data) { + form_data.insert(field_name.to_string(), text); + } + } + + Ok(()) +} + impl Request { - pub fn from_actix_request(req: &HttpRequest, body: Bytes, global_headers: &Headers) -> Self { + pub async fn from_actix_request( + req: &HttpRequest, + mut payload: web::Payload, + global_headers: &Headers, + ) -> Self { let mut query_params: QueryParams = QueryParams::new(); + let mut form_data: HashMap = HashMap::new(); + let mut files = HashMap::new(); + if !req.query_string().is_empty() { let split = req.query_string().split('&'); for s in split { @@ -60,8 +135,41 @@ impl Request { } let mut headers = Headers::from_actix_headers(req.headers()); + debug!("Global headers: {:?}", global_headers); headers.extend(global_headers); + let body: Vec = if headers.contains(String::from("content-type")) + && headers + .get(String::from("content-type")) + .is_ok_and(|val| val == "multipart/form-data") + { + let h = headers.get(String::from("content-type")).unwrap(); + debug!("Content-Type: {:?}", h); + let multipart = Multipart::new(req.headers(), payload); + let mut body_local: Vec = Vec::new(); + + let a = handle_multipart(multipart, &mut files, &mut form_data, &mut body_local).await; + + if let Err(e) = a { + debug!("Error handling multipart data: {:?}", e); + } + + body_local + } else { + let mut body_local = BytesMut::new(); + while let Some(chunk) = payload.next().await { + let chunk = chunk.expect("Failed to read chunk from payload"); + body_local.extend_from_slice(&chunk); + } + body_local.freeze().to_vec() + }; + + debug!("Request body: {:?}", body); + debug!("Request headers: {:?}", headers); + debug!("Request query params: {:?}", query_params); + debug!("Request form data: {:?}", form_data); + debug!("Request files: {:?}", files); + let url = Url::new( req.connection_info().scheme(), req.connection_info().host(), @@ -74,10 +182,12 @@ impl Request { headers, method: req.method().as_str().to_owned(), path_params: HashMap::new(), - body: body.to_vec(), + body, url, ip_addr, identity: None, + form_data: Some(form_data), + files: Some(files), } } } @@ -101,6 +211,10 @@ pub struct PyRequest { pub url: Url, #[pyo3(get)] pub ip_addr: Option, + #[pyo3(get, set)] + pub form_data: Py, + #[pyo3(get, set)] + pub files: Py, } #[pymethods] @@ -114,6 +228,8 @@ impl PyRequest { body: Py, method: String, url: Url, + form_data: Py, + files: Py, identity: Option, ip_addr: Option, ) -> Self { @@ -125,6 +241,8 @@ impl PyRequest { body, method, url, + form_data, + files, ip_addr, } } diff --git a/unit_tests/test_request_object.py b/unit_tests/test_request_object.py index f1f46cd88..aa3f7a117 100644 --- a/unit_tests/test_request_object.py +++ b/unit_tests/test_request_object.py @@ -16,6 +16,8 @@ def test_request_object(): url=url, ip_addr=None, identity=None, + form_data={}, + files={}, ) assert request.url.scheme == "https"