Bläddra i källkod

feat(core): add support to multipart/form-data requests, closes #2118 (#3929)

Lucas Fernandes Nogueira 3 år sedan
förälder
incheckning
1397d9121a

+ 5 - 0
.changes/form-multipart-support.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+The HTTP API now supports `multipart/form-data` requests. You need to set the `Content-Type` header and enable the `http-multipart` Cargo feature.

+ 5 - 0
.changes/refactor-form-part-bytes.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+**Breaking change:** The `tauri::api::http::FormPart::Bytes` enum variant has been renamed to `File` with a value object `{ file, mime, file_name }`.

+ 1 - 1
.github/workflows/lint-fmt-core.yml

@@ -50,7 +50,7 @@ jobs:
         clippy:
           - { args: '', key: 'empty' }
           - {
-              args: '--features compression,wry,isolation,custom-protocol,api-all,cli,updater,system-tray',
+              args: '--features compression,wry,isolation,custom-protocol,api-all,cli,updater,system-tray,http-multipart',
               key: 'all'
             }
           - {

+ 1 - 1
.github/workflows/test-core.yml

@@ -89,4 +89,4 @@ jobs:
         run: |
           cargo test
           cargo test --features api-all
-          cargo test --features compression,wry,isolation,custom-protocol,api-all,cli,updater,system-tray
+          cargo test --features compression,wry,isolation,custom-protocol,api-all,cli,updater,system-tray,http-multipart

+ 1 - 1
.github/workflows/udeps.yml

@@ -29,7 +29,7 @@ jobs:
         clippy:
           - {
               path: './core/tauri/Cargo.toml',
-              args: '--features compression,wry,isolation,custom-protocol,api-all,cli,updater,system-tray'
+              args: '--features compression,wry,isolation,custom-protocol,api-all,cli,updater,system-tray,http-multipart'
             }
           - { path: './core/tauri-build/Cargo.toml', args: '--all-features' }
           - { path: './core/tauri-codegen/Cargo.toml', args: '--all-features' }

+ 4 - 2
core/tauri/Cargo.toml

@@ -28,6 +28,7 @@ features = [
   "__updater-docs",
   "system-tray",
   "devtools",
+  "http-multipart",
   "dox"
 ]
 rustdoc-args = [ "--cfg", "doc_cfg" ]
@@ -39,7 +40,7 @@ targets = [
 ]
 
 [package.metadata.cargo-udeps.ignore]
-normal = [ "attohttpc" ]
+normal = [ "attohttpc", "reqwest" ]
 
 [dependencies]
 serde_json = { version = "1.0", features = [ "raw_value" ] }
@@ -72,7 +73,7 @@ percent-encoding = "2.1"
 base64 = { version = "0.13", optional = true }
 clap = { version = "3", optional = true }
 notify-rust = { version = "4.5", optional = true }
-reqwest = { version = "0.11", features = [ "json", "multipart", "stream" ], optional = true }
+reqwest = { version = "0.11", features = [ "json", "stream" ], optional = true }
 bytes = { version = "1", features = [ "serde" ], optional = true }
 attohttpc = { version = "0.19", features = [ "json", "form" ], optional = true }
 open = { version = "2.0", optional = true }
@@ -134,6 +135,7 @@ updater = [
 ]
 __updater-docs = [ "minisign-verify", "base64", "http-api", "dialog-ask" ]
 http-api = [ "attohttpc" ]
+http-multipart = [ "attohttpc/multipart-form", "reqwest/multipart" ]
 shell-open-api = [ "open", "regex", "tauri-macros/shell-scope" ]
 fs-extract-api = [ "zip" ]
 reqwest-client = [ "reqwest", "bytes" ]

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
core/tauri/scripts/bundle.js


+ 152 - 18
core/tauri/src/api/http.rs

@@ -11,7 +11,7 @@ use serde_json::Value;
 use serde_repr::{Deserialize_repr, Serialize_repr};
 use url::Url;
 
-use std::{collections::HashMap, time::Duration};
+use std::{collections::HashMap, path::PathBuf, time::Duration};
 
 #[cfg(feature = "reqwest-client")]
 pub use reqwest::header;
@@ -114,7 +114,7 @@ impl Client {
       request_builder = request_builder.params(&query);
     }
 
-    if let Some(headers) = request.headers {
+    if let Some(headers) = &request.headers {
       for (name, value) in headers.0.iter() {
         request_builder = request_builder.header(name, value);
       }
@@ -130,14 +130,69 @@ impl Client {
         Body::Text(text) => request_builder.body(attohttpc::body::Bytes(text)).send()?,
         Body::Json(json) => request_builder.json(&json)?.send()?,
         Body::Form(form_body) => {
-          let mut form = Vec::new();
-          for (name, part) in form_body.0 {
-            match part {
-              FormPart::Bytes(bytes) => form.push((name, serde_json::to_string(&bytes)?)),
-              FormPart::Text(text) => form.push((name, text)),
+          #[allow(unused_variables)]
+          fn send_form(
+            request_builder: attohttpc::RequestBuilder,
+            headers: &Option<HeaderMap>,
+            form_body: FormBody,
+          ) -> crate::api::Result<attohttpc::Response> {
+            #[cfg(feature = "http-multipart")]
+            if matches!(
+              headers
+                .as_ref()
+                .and_then(|h| h.0.get("content-type"))
+                .map(|v| v.as_bytes()),
+              Some(b"multipart/form-data")
+            ) {
+              let mut multipart = attohttpc::MultipartBuilder::new();
+              let mut byte_cache: HashMap<String, Vec<u8>> = Default::default();
+
+              for (name, part) in &form_body.0 {
+                if let FormPart::File { file, .. } = part {
+                  byte_cache.insert(name.to_string(), file.clone().try_into()?);
+                }
+              }
+              for (name, part) in &form_body.0 {
+                multipart = match part {
+                  FormPart::File {
+                    file,
+                    mime,
+                    file_name,
+                  } => {
+                    // safe to unwrap: always set by previous loop
+                    let mut file =
+                      attohttpc::MultipartFile::new(name, byte_cache.get(name).unwrap());
+                    if let Some(mime) = mime {
+                      file = file.with_type(mime)?;
+                    }
+                    if let Some(file_name) = file_name {
+                      file = file.with_filename(file_name);
+                    }
+                    multipart.with_file(file)
+                  }
+                  FormPart::Text(value) => multipart.with_text(name, value),
+                };
+              }
+              return request_builder
+                .body(multipart.build()?)
+                .send()
+                .map_err(Into::into);
+            }
+
+            let mut form = Vec::new();
+            for (name, part) in form_body.0 {
+              match part {
+                FormPart::File { file, .. } => {
+                  let bytes: Vec<u8> = file.try_into()?;
+                  form.push((name, serde_json::to_string(&bytes)?))
+                }
+                FormPart::Text(value) => form.push((name, value)),
+              }
             }
+            request_builder.form(&form)?.send().map_err(Into::into)
           }
-          request_builder.form(&form)?.send()?
+
+          send_form(request_builder, &request.headers, form_body)?
         }
       }
     } else {
@@ -176,14 +231,61 @@ impl Client {
         Body::Text(text) => request_builder.body(bytes::Bytes::from(text)),
         Body::Json(json) => request_builder.json(&json),
         Body::Form(form_body) => {
-          let mut form = Vec::new();
-          for (name, part) in form_body.0 {
-            match part {
-              FormPart::Bytes(bytes) => form.push((name, serde_json::to_string(&bytes)?)),
-              FormPart::Text(text) => form.push((name, text)),
+          #[allow(unused_variables)]
+          fn send_form(
+            request_builder: reqwest::RequestBuilder,
+            headers: &Option<HeaderMap>,
+            form_body: FormBody,
+          ) -> crate::api::Result<reqwest::RequestBuilder> {
+            #[cfg(feature = "http-multipart")]
+            if matches!(
+              headers
+                .as_ref()
+                .and_then(|h| h.0.get("content-type"))
+                .map(|v| v.as_bytes()),
+              Some(b"multipart/form-data")
+            ) {
+              let mut multipart = reqwest::multipart::Form::new();
+
+              for (name, part) in form_body.0 {
+                let part = match part {
+                  FormPart::File {
+                    file,
+                    mime,
+                    file_name,
+                  } => {
+                    let bytes: Vec<u8> = file.try_into()?;
+                    let mut part = reqwest::multipart::Part::bytes(bytes);
+                    if let Some(mime) = mime {
+                      part = part.mime_str(&mime)?;
+                    }
+                    if let Some(file_name) = file_name {
+                      part = part.file_name(file_name);
+                    }
+                    part
+                  }
+                  FormPart::Text(value) => reqwest::multipart::Part::text(value),
+                };
+
+                multipart = multipart.part(name, part);
+              }
+
+              return Ok(request_builder.multipart(multipart));
             }
+
+            let mut form = Vec::new();
+            for (name, part) in form_body.0 {
+              match part {
+                FormPart::File { file, .. } => {
+                  let bytes: Vec<u8> = file.try_into()?;
+                  form.push((name, serde_json::to_string(&bytes)?))
+                }
+                FormPart::Text(value) => form.push((name, value)),
+              }
+            }
+            Ok(request_builder.form(&form))
           }
-          request_builder.form(&form)
+          send_form(request_builder, &request.headers, form_body)?
         }
       };
     }
@@ -216,6 +318,28 @@ pub enum ResponseType {
   Binary,
 }
 
+/// A file path or contents.
+#[derive(Debug, Clone, Deserialize)]
+#[serde(untagged)]
+#[non_exhaustive]
+pub enum FilePart {
+  /// File path.
+  Path(PathBuf),
+  /// File contents.
+  Contents(Vec<u8>),
+}
+
+impl TryFrom<FilePart> for Vec<u8> {
+  type Error = crate::api::Error;
+  fn try_from(file: FilePart) -> crate::api::Result<Self> {
+    let bytes = match file {
+      FilePart::Path(path) => std::fs::read(&path)?,
+      FilePart::Contents(bytes) => bytes,
+    };
+    Ok(bytes)
+  }
+}
+
 /// [`FormBody`] data types.
 #[derive(Debug, Deserialize)]
 #[serde(untagged)]
@@ -223,13 +347,23 @@ pub enum ResponseType {
 pub enum FormPart {
   /// A string value.
   Text(String),
-  /// A byte array value.
-  Bytes(Vec<u8>),
+  /// A file value.
+  #[serde(rename_all = "camelCase")]
+  File {
+    /// File path or content.
+    file: FilePart,
+    /// Mime type of this part.
+    /// Only used when the `Content-Type` header is set to `multipart/form-data`.
+    mime: Option<String>,
+    /// File name.
+    /// Only used when the `Content-Type` header is set to `multipart/form-data`.
+    file_name: Option<String>,
+  },
 }
 
 /// Form body definition.
 #[derive(Debug, Deserialize)]
-pub struct FormBody(HashMap<String, FormPart>);
+pub struct FormBody(pub(crate) HashMap<String, FormPart>);
 
 impl FormBody {
   /// Creates a new form body.
@@ -243,7 +377,7 @@ impl FormBody {
 #[serde(tag = "type", content = "payload")]
 #[non_exhaustive]
 pub enum Body {
-  /// A multipart formdata body.
+  /// A form body.
   Form(FormBody),
   /// A JSON body.
   Json(Value),

+ 19 - 7
core/tauri/src/endpoints/http.rs

@@ -79,19 +79,31 @@ impl Cmd {
     options: Box<HttpRequestBuilder>,
   ) -> super::Result<ResponseData> {
     use crate::Manager;
-    if context
-      .window
-      .state::<crate::Scopes>()
-      .http
-      .is_allowed(&options.url)
-    {
+    let scopes = context.window.state::<crate::Scopes>();
+    if scopes.http.is_allowed(&options.url) {
       let client = clients()
         .lock()
         .unwrap()
         .get(&client_id)
         .ok_or_else(|| crate::Error::HttpClientNotInitialized.into_anyhow())?
         .clone();
-      let response = client.send(*options).await?;
+      let options = *options;
+      if let Some(crate::api::http::Body::Form(form)) = &options.body {
+        for value in form.0.values() {
+          if let crate::api::http::FormPart::File {
+            file: crate::api::http::FilePart::Path(path),
+            ..
+          } = value
+          {
+            if crate::api::file::SafePathBuf::new(path.clone()).is_err()
+              || scopes.fs.is_allowed(&path)
+            {
+              return Err(crate::Error::PathNotAllowed(path.clone()).into_anyhow());
+            }
+          }
+        }
+      }
+      let response = client.send(options).await?;
       Ok(response.read().await?)
     } else {
       Err(crate::Error::UrlNotAllowed(options.url).into_anyhow())

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
examples/api/dist/assets/index.js


+ 1 - 0
examples/api/dist/assets/vendor.css

@@ -0,0 +1 @@
+ul.svelte-gbh3pt{list-style:none;margin:0;padding:0;padding-left:var(--nodePaddingLeft, 1rem);border-left:var(--nodeBorderLeft, 1px dotted #9ca3af);color:var(--nodeColor, #374151)}.hidden.svelte-gbh3pt{display:none}.bracket.svelte-gbh3pt{cursor:pointer}.bracket.svelte-gbh3pt:hover{background:var(--bracketHoverBackground, #d1d5db)}.comma.svelte-gbh3pt{color:var(--nodeColor, #374151)}.val.svelte-gbh3pt{color:var(--leafDefaultColor, #9ca3af)}.val.string.svelte-gbh3pt{color:var(--leafStringColor, #059669)}.val.number.svelte-gbh3pt{color:var(--leafNumberColor, #d97706)}.val.boolean.svelte-gbh3pt{color:var(--leafBooleanColor, #2563eb)}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
examples/api/dist/assets/vendor.js


+ 6 - 2
examples/api/dist/global.css

@@ -138,14 +138,14 @@ main {
   height: 100%;
 }
 
-[type='radio']:checked ~ label {
+[type='radio']:checked~label {
   background: rgb(36, 37, 38);
   color: #67d6ed;
   border-bottom: 1px solid transparent;
   z-index: 2;
 }
 
-[type='radio']:checked ~ label ~ .content {
+[type='radio']:checked~label~.content {
   z-index: 1;
 }
 
@@ -190,3 +190,7 @@ main {
 #file-response {
   height: 400px;
 }
+
+span.key {
+  color: #fff;
+}

+ 1 - 0
examples/api/dist/index.html

@@ -7,6 +7,7 @@
     <title>Svelte + Vite App</title>
     <script type="module" crossorigin src="/assets/index.js"></script>
     <link rel="modulepreload" href="/assets/vendor.js">
+    <link rel="stylesheet" href="/assets/vendor.css">
     <link rel="stylesheet" href="/assets/index.css">
   </head>
 

+ 3 - 2
examples/api/package.json

@@ -10,11 +10,12 @@
   },
   "dependencies": {
     "@tauri-apps/api": "../../tooling/api/dist",
+    "@zerodevx/svelte-json-view": "0.2.0",
     "hotkeys-js": "^3.8.5"
   },
   "devDependencies": {
-    "svelte": "3.35.0",
     "@sveltejs/vite-plugin-svelte": "^1.0.0-next.11",
+    "svelte": "3.35.0",
     "vite": "^2.6.4"
   }
-}
+}

+ 6 - 2
examples/api/public/global.css

@@ -138,14 +138,14 @@ main {
   height: 100%;
 }
 
-[type='radio']:checked ~ label {
+[type='radio']:checked~label {
   background: rgb(36, 37, 38);
   color: #67d6ed;
   border-bottom: 1px solid transparent;
   z-index: 2;
 }
 
-[type='radio']:checked ~ label ~ .content {
+[type='radio']:checked~label~.content {
   z-index: 1;
 }
 
@@ -190,3 +190,7 @@ main {
 #file-response {
   height: 400px;
 }
+
+span.key {
+  color: #fff;
+}

+ 322 - 0
examples/api/src-tauri/Cargo.lock

@@ -96,8 +96,15 @@ dependencies = [
  "serde_json",
  "tauri",
  "tauri-build",
+ "tiny_http",
 ]
 
+[[package]]
+name = "ascii"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbf56136a5198c7b01a49e3afcbef6cf84597273d298f54432926024107b0109"
+
 [[package]]
 name = "async-broadcast"
 version = "0.3.4"
@@ -223,6 +230,8 @@ dependencies = [
  "flate2",
  "http",
  "log",
+ "mime",
+ "multipart",
  "native-tls",
  "openssl",
  "serde",
@@ -332,6 +341,9 @@ name = "bytes"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "cache-padded"
@@ -435,6 +447,12 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
 
+[[package]]
+name = "chunked_transfer"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
+
 [[package]]
 name = "cipher"
 version = "0.3.0"
@@ -839,6 +857,15 @@ version = "1.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
 
+[[package]]
+name = "encoding_rs"
+version = "0.8.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
+dependencies = [
+ "cfg-if",
+]
+
 [[package]]
 name = "enumflags2"
 version = "0.7.4"
@@ -1384,6 +1411,25 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "h2"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util 0.7.1",
+ "tracing",
+]
+
 [[package]]
 name = "hashbrown"
 version = "0.11.2"
@@ -1445,12 +1491,72 @@ dependencies = [
  "itoa 1.0.1",
 ]
 
+[[package]]
+name = "http-body"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
 [[package]]
 name = "http-range"
 version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
 
+[[package]]
+name = "httparse"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6330e8a36bd8c859f3fa6d9382911fbb7147ec39807f63b923933a247240b9ba"
+
+[[package]]
+name = "httpdate"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+
+[[package]]
+name = "hyper"
+version = "0.14.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa 1.0.1",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
 [[package]]
 name = "ico"
 version = "0.1.0"
@@ -1533,6 +1639,12 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "ipnet"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
+
 [[package]]
 name = "itertools"
 version = "0.10.3"
@@ -1781,6 +1893,22 @@ dependencies = [
  "autocfg",
 ]
 
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
 [[package]]
 name = "minisign-verify"
 version = "0.2.0"
@@ -1806,6 +1934,42 @@ dependencies = [
  "adler",
 ]
 
+[[package]]
+name = "mio"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9"
+dependencies = [
+ "libc",
+ "log",
+ "miow",
+ "ntapi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+ "winapi",
+]
+
+[[package]]
+name = "miow"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "multipart"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182"
+dependencies = [
+ "log",
+ "mime",
+ "mime_guess",
+ "rand 0.8.5",
+ "tempfile",
+]
+
 [[package]]
 name = "native-tls"
 version = "0.2.9"
@@ -1916,6 +2080,15 @@ dependencies = [
  "zvariant_derive",
 ]
 
+[[package]]
+name = "ntapi"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "num-integer"
 version = "0.1.44"
@@ -2609,6 +2782,44 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "reqwest"
+version = "0.11.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-tls",
+ "ipnet",
+ "js-sys",
+ "lazy_static",
+ "log",
+ "mime",
+ "mime_guess",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "tokio",
+ "tokio-native-tls",
+ "tokio-util 0.6.9",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "winreg",
+]
+
 [[package]]
 name = "rfd"
 version = "0.8.1"
@@ -3190,6 +3401,7 @@ dependencies = [
  "attohttpc",
  "base64",
  "bincode",
+ "bytes",
  "cfg_aliases",
  "clap",
  "dirs-next",
@@ -3216,6 +3428,7 @@ dependencies = [
  "rand 0.8.5",
  "raw-window-handle",
  "regex",
+ "reqwest",
  "rfd",
  "semver 1.0.7",
  "serde",
@@ -3420,8 +3633,29 @@ version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd"
 dependencies = [
+ "itoa 1.0.1",
  "libc",
  "num_threads",
+ "time-macros",
+]
+
+[[package]]
+name = "time-macros"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
+
+[[package]]
+name = "tiny_http"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0d6ef4e10d23c1efb862eecad25c5054429a71958b4eeef85eb5e7170b477ca"
+dependencies = [
+ "ascii",
+ "chunked_transfer",
+ "log",
+ "time",
+ "url",
 ]
 
 [[package]]
@@ -3446,9 +3680,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee"
 dependencies = [
  "bytes",
+ "libc",
  "memchr",
+ "mio",
  "num_cpus",
  "pin-project-lite",
+ "socket2",
+ "winapi",
+]
+
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "log",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0edfdeb067411dba2044da6d1cb2df793dd35add7888d73c16e3381ded401764"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+ "tracing",
 ]
 
 [[package]]
@@ -3460,6 +3736,12 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "tower-service"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
+
 [[package]]
 name = "tracing"
 version = "0.1.32"
@@ -3531,6 +3813,12 @@ dependencies = [
  "serde_json",
 ]
 
+[[package]]
+name = "try-lock"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
+
 [[package]]
 name = "typenum"
 version = "1.15.0"
@@ -3543,6 +3831,15 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
 
+[[package]]
+name = "unicase"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
+dependencies = [
+ "version_check",
+]
+
 [[package]]
 name = "unicode-bidi"
 version = "0.3.7"
@@ -3661,6 +3958,16 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "want"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
+dependencies = [
+ "log",
+ "try-lock",
+]
+
 [[package]]
 name = "wasi"
 version = "0.9.0+wasi-snapshot-preview1"
@@ -3673,6 +3980,12 @@ version = "0.10.2+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
 
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
 [[package]]
 name = "wasm-bindgen"
 version = "0.2.79"
@@ -4087,6 +4400,15 @@ version = "0.33.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ff1e4aa646495048ec7f3ffddc411e1d829c026a2ec62b39da15c1055e406eaa"
 
+[[package]]
+name = "winreg"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "winres"
 version = "0.1.12"

+ 2 - 1
examples/api/src-tauri/Cargo.toml

@@ -12,7 +12,8 @@ tauri-build = { path = "../../../core/tauri-build", features = ["isolation"] }
 [dependencies]
 serde_json = "1.0"
 serde = { version = "1.0", features = [ "derive" ] }
-tauri = { path = "../../../core/tauri", default-features = false, features = ["api-all", "cli", "compression", "icon-ico", "icon-png", "isolation", "macos-private-api", "objc-exception", "system-tray", "updater", "wry"] }
+tauri = { path = "../../../core/tauri", default-features = false, features = ["api-all", "http-multipart", "reqwest-client", "cli", "compression", "icon-ico", "icon-png", "isolation", "macos-private-api", "objc-exception", "system-tray", "updater", "wry"] }
+tiny_http = "0.11"
 
 [features]
 default = [ "custom-protocol", "tauri/ayatana-tray" ]

+ 28 - 34
examples/api/src-tauri/src/main.rs

@@ -14,11 +14,10 @@ mod menu;
 use std::path::PathBuf;
 use std::sync::atomic::{AtomicBool, Ordering};
 
-use serde::{Deserialize, Serialize};
+use serde::Serialize;
 use tauri::{
-  api::dialog::ask, http::ResponseBuilder, window::WindowBuilder, CustomMenuItem,
-  GlobalShortcutManager, Manager, RunEvent, SystemTray, SystemTrayEvent, SystemTrayMenu,
-  WindowEvent, WindowUrl,
+  api::dialog::ask, window::WindowBuilder, CustomMenuItem, GlobalShortcutManager, Manager,
+  RunEvent, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowEvent, WindowUrl,
 };
 
 #[derive(Clone, Serialize)]
@@ -26,18 +25,6 @@ struct Reply {
   data: String,
 }
 
-#[derive(Serialize, Deserialize)]
-struct HttpPost {
-  foo: String,
-  bar: String,
-}
-
-#[derive(Serialize)]
-struct HttpReply {
-  msg: String,
-  request: HttpPost,
-}
-
 #[tauri::command]
 async fn menu_toggle(window: tauri::Window) {
   window.menu_handle().toggle().unwrap();
@@ -63,6 +50,31 @@ fn main() {
     .setup(|app| {
       #[cfg(debug_assertions)]
       app.get_window("main").unwrap().open_devtools();
+
+      std::thread::spawn(|| {
+        let server = match tiny_http::Server::http("localhost:3003") {
+          Ok(s) => s,
+          Err(e) => {
+            eprintln!("{}", e);
+            std::process::exit(1);
+          }
+        };
+        loop {
+          if let Ok(mut request) = server.recv() {
+            let mut body = Vec::new();
+            let _ = request.as_reader().read_to_end(&mut body);
+            let response = tiny_http::Response::new(
+              tiny_http::StatusCode(200),
+              request.headers().to_vec(),
+              std::io::Cursor::new(body),
+              request.body_length(),
+              None,
+            );
+            let _ = request.respond(response);
+          }
+        }
+      });
+
       Ok(())
     })
     .on_page_load(|window, _| {
@@ -78,24 +90,6 @@ fn main() {
           .expect("failed to emit");
       });
     })
-    .register_uri_scheme_protocol("customprotocol", move |_app_handle, request| {
-      if request.method() == "POST" {
-        let request: HttpPost = serde_json::from_slice(request.body()).unwrap();
-        return ResponseBuilder::new()
-          .mimetype("application/json")
-          .header("Access-Control-Allow-Origin", "*")
-          .status(200)
-          .body(serde_json::to_vec(&HttpReply {
-            request,
-            msg: "Hello from rust!".to_string(),
-          })?);
-      }
-
-      ResponseBuilder::new()
-        .mimetype("text/html")
-        .status(404)
-        .body(Vec::new())
-    })
     .menu(menu::get_menu())
     .on_menu_event(|event| {
       println!("{:?}", event.menu_item_id());

+ 1 - 1
examples/api/src-tauri/tauri.conf.json

@@ -112,7 +112,7 @@
         }
       },
       "http": {
-        "scope": ["https://jsonplaceholder.typicode.com/todos/*"]
+        "scope": ["http://localhost:3003"]
       }
     },
     "windows": [

+ 1 - 8
examples/api/src/components/Http.svelte

@@ -1,7 +1,6 @@
 <script>
   import { getClient, Body } from "@tauri-apps/api/http";
   let httpMethod = "GET";
-  let httpUrl = "https://jsonplaceholder.typicode.com/todos/1";
   let httpBody = "";
 
   export let onMessage;
@@ -12,10 +11,9 @@
       throw e
     });
     let method = httpMethod || "GET";
-    let url = httpUrl || "";
 
     const options = {
-      url: url || "",
+      url: "http://localhost:3003",
       method: method || "GET",
     };
 
@@ -40,11 +38,6 @@
     <option value="PATCH">PATCH</option>
     <option value="DELETE">DELETE</option>
   </select>
-  <input
-    id="request-url"
-    placeholder="Type the request URL..."
-    bind:value={httpUrl}
-  />
   <br />
   <textarea
     id="request-body"

+ 26 - 19
examples/api/src/components/HttpForm.svelte

@@ -1,32 +1,39 @@
 <script>
+	import { getClient, Body, ResponseType } from "@tauri-apps/api/http"
+	import { JsonView } from '@zerodevx/svelte-json-view'
 	let foo = 'baz'
 	let bar = 'qux'
 	let result = null
+	let multipart = true
 	
 	async function doPost () {
-    let url = navigator.userAgent.includes("Windows") ? "https://customprotocol.test/example.html" : "customprotocol://test/example.html";
-		const res = await fetch(url, {
+		const client = await getClient().catch(e => {
+      onMessage(e)
+      throw e
+    })
+		
+		result = await client.request({
+			url: 'http://localhost:3003',
 			method: 'POST',
-			body: JSON.stringify({
+			body: Body.form({
 				foo,
 				bar
-			})
+			}),
+			headers: multipart ? { 'Content-Type': 'multipart/form-data' } : undefined,
+			responseType: ResponseType.Text
 		})
-		
-		const json = await res.json()
-		result = JSON.stringify(json)
 	}
 </script>
 
-
-<input bind:value={foo} />
-<input bind:value={bar} />
-<button type="button" on:click={doPost}>
-	Post it.
-</button>
-<p>
-	Result:
-</p>
-<pre>
-{result}
-</pre>
+<div>
+	<input bind:value={foo} />
+	<input bind:value={bar} />
+	<label>
+		<input type="checkbox" bind:checked={multipart} />
+		Multipart
+	</label>
+	<button type="button" on:click={doPost}>
+		Post it.
+	</button>
+	<JsonView json={result} />
+</div>

+ 11 - 6
examples/api/yarn.lock

@@ -23,9 +23,14 @@
     svelte-hmr "^0.14.7"
 
 "@tauri-apps/api@../../tooling/api/dist":
-  version "1.0.0-rc.3"
+  version "1.0.0-rc.0"
   dependencies:
-    type-fest "2.12.2"
+    type-fest "2.11.2"
+
+"@zerodevx/svelte-json-view@0.2.0":
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/@zerodevx/svelte-json-view/-/svelte-json-view-0.2.0.tgz#e4d1f42d3a0fb57c921873fe9e703dbd5d08d989"
+  integrity sha512-hAqzbaDJ+cMDAID0PHr8JT0guSaPsabhVxssy5J1mAqrmUvgrAbxxgTT0RzHSwkACrRn9LP8YDbxrwbHdW0FYw==
 
 debug@^4.3.2:
   version "4.3.3"
@@ -262,10 +267,10 @@ svelte@3.35.0:
   resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.35.0.tgz#e0d0ba60c4852181c2b4fd851194be6fda493e65"
   integrity sha512-gknlZkR2sXheu/X+B7dDImwANVvK1R0QGQLd8CNIfxxGPeXBmePnxfzb6fWwTQRsYQG7lYkZXvpXJvxvpsoB7g==
 
-type-fest@2.12.2:
-  version "2.12.2"
-  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.12.2.tgz#80a53614e6b9b475eb9077472fb7498dc7aa51d0"
-  integrity sha512-qt6ylCGpLjZ7AaODxbpyBZSs9fCI9SkL3Z9q2oxMBQhs/uyY+VD8jHA8ULCGmWQJlBgqvO3EJeAngOHD8zQCrQ==
+type-fest@2.11.2:
+  version "2.11.2"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.11.2.tgz#5534a919858bc517492cd3a53a673835a76d2e71"
+  integrity sha512-reW2Y2Mpn0QNA/5fvtm5doROLwDPu2zOm5RtY7xQQS05Q7xgC8MOZ3yPNaP9m/s/sNjjFQtHo7VCNqYW2iI+Ig==
 
 vite@^2.6.4:
   version "2.6.14"

+ 40 - 4
tooling/api/src/http.ts

@@ -56,7 +56,13 @@ enum ResponseType {
   Binary = 3
 }
 
-type Part = string | Uint8Array
+interface FilePart<T> {
+  value: string | T
+  mime?: string
+  fileName?: string
+}
+
+type Part = string | Uint8Array | FilePart<Uint8Array>
 
 /** The body object to be used on POST and PUT requests. */
 class Body {
@@ -70,19 +76,49 @@ class Body {
   }
 
   /**
-   * Creates a new form data body.
+   * Creates a new form data body. The form data is an object where each key is the entry name,
+   * and the value is either a string or a file object.
+   *
+   * By default it sets the `application/x-www-form-urlencoded` Content-Type header,
+   * but you can set it to `multipart/form-data` if the Cargo feature `http-multipart` is enabled.
+   *
+   * Note that a file path must be allowed in the `fs` allowlist scope.
+   *
+   * # Examples
+   *
+   * ```js
+   * import { Body } from "@tauri-apps/api/http"
+   * Body.form({
+   *   key: 'value',
+   *   image: {
+   *     file: '/path/to/file', // either a path of an array buffer of the file contents
+   *     mime: 'image/jpeg', // optional
+   *     fileName: 'image.jpg' // optional
+   *   }
+   * })
+   * ```
    *
    * @param data The body data.
    *
    * @return The body object ready to be used on the POST and PUT requests.
    */
   static form(data: Record<string, Part>): Body {
-    const form: Record<string, string | number[]> = {}
+    const form: Record<string, string | number[] | FilePart<number[]>> = {}
     for (const key in data) {
       // eslint-disable-next-line security/detect-object-injection
       const v = data[key]
+      let r
+      if (typeof v === 'string') {
+        r = v
+      } else if (v instanceof Uint8Array || Array.isArray(v)) {
+        r = Array.from(v)
+      } else if (typeof v.value === 'string') {
+        r = { value: v.value, mime: v.mime, fileName: v.fileName }
+      } else {
+        r = { value: Array.from(v.value), mime: v.mime, fileName: v.fileName }
+      }
       // eslint-disable-next-line security/detect-object-injection
-      form[key] = typeof v === 'string' ? v : Array.from(v)
+      form[key] = r
     }
     return new Body('Form', form)
   }

Vissa filer visades inte eftersom för många filer har ändrats