feat(tauri) add httpRequest API (#589)

This commit is contained in:
Lucas Fernandes Nogueira 2020-05-11 17:03:58 -03:00 committed by GitHub
parent b9277ea53f
commit 4b54cc1564
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 565 additions and 75 deletions

160
cli/tauri.js/api/http.js Normal file
View File

@ -0,0 +1,160 @@
import tauri from './tauri'
/**
* @typedef {number} ResponseType
*/
/**
* @enum {ResponseType}
*/
const ResponseType = {
JSON: 1,
Text: 2,
Binary: 3
}
/**
* @typedef {number} BodyType
*/
/**
* @enum {BodyType}
*/
const BodyType = {
Form: 1,
File: 2,
Auto: 3
}
/**
* @typedef {Object} HttpOptions
* @property {String} options.method GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT or TRACE
* @property {String} options.url the request URL
* @property {Object} [options.headers] the request headers
* @property {Object} [options.propertys] the request query propertys
* @property {Object|String|Binary} [options.body] the request body
* @property {Boolean} followRedirects whether to follow redirects or not
* @property {Number} maxRedirections max number of redirections
* @property {Number} connectTimeout request connect timeout
* @property {Number} readTimeout request read timeout
* @property {Number} timeout request timeout
* @property {Boolean} allowCompression
* @property {ResponseType} [responseType=1] response type
* @property {BodyType} [bodyType=3] body type
*/
/**
* makes a HTTP request
*
* @param {HttpOptions} options request options
*
* @return {Promise<any>} promise resolving to the response
*/
function request (options) {
return tauri.httpRequest(options)
}
/**
* makes a GET request
*
* @param {String} url request URL
* @param {String|Object|Binary} body request body
* @param {HttpOptions} options request options
*
* @return {Promise<any>} promise resolving to the response
*/
function get (url, options = {}) {
return request({
method: 'GET',
url,
...options
})
}
/**
* makes a POST request
*
* @param {String} url request URL
* @param {String|Object|Binary} body request body
* @param {HttpOptions} options request options
*
* @return {Promise<any>} promise resolving to the response
*/
function post (url, body = void 0, options = {}) {
return request({
method: 'POST',
url,
body,
...options
})
}
/**
* makes a PUT request
*
* @param {String} url request URL
* @param {String|Object|Binary} body request body
* @param {HttpOptions} options request options
*
* @return {Promise<any>} promise resolving to the response
*/
function put (url, body = void 0, options = {}) {
return request({
method: 'PUT',
url,
body,
...options
})
}
/**
* makes a PATCH request
*
* @param {String} url request URL
* @param {HttpOptions} options request options
*
* @return {Promise<any>} promise resolving to the response
*/
function patch (url, options = {}) {
return request({
method: 'PATCH',
url,
...options
})
}
/**
* makes a DELETE request
*
* @param {String} url request URL
* @param {HttpOptions} options request options
*
* @return {Promise<any>} promise resolving to the response
*/
function deleteRequest (url, options = {}) {
return request({
method: 'DELETE',
url,
...options
})
}
export {
request,
get,
post,
put,
patch,
deleteRequest,
ResponseType,
BodyType
}
export default {
request,
get,
post,
put,
patch,
delete: deleteRequest,
ResponseType,
BodyType
}

View File

@ -100,6 +100,9 @@ var Dir = {
} }
<% if (ctx.dev) { %> <% if (ctx.dev) { %>
function camelToKebab (string) {
return string.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase()
}
/** /**
* @name return __whitelistWarning * @name return __whitelistWarning
* @description Present a stylish warning to the developer that their API * @description Present a stylish warning to the developer that their API
@ -108,7 +111,7 @@ var Dir = {
* @private * @private
*/ */
var __whitelistWarning = function (func) { var __whitelistWarning = function (func) {
console.warn('%c[Tauri] Danger \ntauri.' + func + ' not whitelisted 💣\n%c\nAdd to tauri.conf.json: \n\ntauri: \n whitelist: { \n ' + func + ': true \n\nReference: https://github.com/tauri-apps/tauri/wiki' + func, 'background: red; color: white; font-weight: 800; padding: 2px; font-size:1.5em', ' ') console.warn('%c[Tauri] Danger \ntauri.' + func + ' not whitelisted 💣\n%c\nAdd to tauri.conf.json: \n\ntauri: \n whitelist: { \n ' + camelToKebab(func) + ': true \n\nReference: https://github.com/tauri-apps/tauri/wiki' + func, 'background: red; color: white; font-weight: 800; padding: 2px; font-size:1.5em', ' ')
return __reject() return __reject()
} }
<% } %> <% } %>
@ -467,8 +470,8 @@ window.tauri = {
<% if (tauri.whitelist.renameFile === true || tauri.whitelist.all === true) { %> <% if (tauri.whitelist.renameFile === true || tauri.whitelist.all === true) { %>
return this.promisified({ return this.promisified({
cmd: 'renameFile', cmd: 'renameFile',
old_path: oldPath, oldPath: oldPath,
new_path: newPath, newPath: newPath,
options: options options: options
}); });
<% } else { %> <% } else { %>
@ -611,13 +614,48 @@ window.tauri = {
<% } %> <% } %>
}, },
loadAsset: function loadAsset(assetName, assetType) { <% if (ctx.dev) { %>
return this.promisified({ /**
cmd: 'loadAsset', * @name httpRequest
asset: assetName, * @description Makes an HTTP request
asset_type: assetType || 'unknown' * @param {Object} options
}) * @param {String} options.method GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT or TRACE
} * @param {String} options.url the request URL
* @param {Object} [options.headers] the request headers
* @param {Object} [options.params] the request query params
* @param {Object|String|Binary} [options.body] the request body
* @param {Boolean} followRedirects whether to follow redirects or not
* @param {Number} maxRedirections max number of redirections
* @param {Number} connectTimeout request connect timeout
* @param {Number} readTimeout request read timeout
* @param {Number} timeout request timeout
* @param {Boolean} allowCompression
* @param {Number} [responseType=1] 1 - JSON, 2 - Text, 3 - Binary
* @param {Number} [bodyType=3] 1 - Form, 2 - File, 3 - Auto
* @returns {Promise<any>}
*/
<% } %>
httpRequest: function httpRequest(options) {
<% if (tauri.whitelist.readBinaryFile === true || tauri.whitelist.all === true) { %>
return this.promisified({
cmd: 'httpRequest',
options: options
});
<% } else { %>
<% if (ctx.dev) { %>
return __whitelistWarning('httpRequest')
<% } %>
return __reject()
<% } %>
},
loadAsset: function loadAsset(assetName, assetType) {
return this.promisified({
cmd: 'loadAsset',
asset: assetName,
assetType: assetType || 'unknown'
})
}
}; };
// init tauri API // init tauri API

View File

@ -11,6 +11,7 @@ exclude = ["test/fixture/**"]
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_repr = "0.1" serde_repr = "0.1"
dirs = "2.0.2" dirs = "2.0.2"
ignore = "0.4.11" ignore = "0.4.11"
@ -24,6 +25,8 @@ flate2 = "1"
error-chain = "0.12" error-chain = "0.12"
rand = "0.7" rand = "0.7"
nfd = "0.0.4" nfd = "0.0.4"
attohttpc = {version = "0.13.0", features=["json", "form" ]}
http = "0.2"
tauri-utils = {version = "0.5", path = "../tauri-utils"} tauri-utils = {version = "0.5", path = "../tauri-utils"}
[dev-dependencies] [dev-dependencies]

147
tauri-api/src/http.rs Normal file
View File

@ -0,0 +1,147 @@
use std::time::Duration;
use std::fs::File;
use std::collections::HashMap;
use attohttpc::{RequestBuilder, Method};
use http::header::HeaderName;
use serde_json::Value;
use serde::Deserialize;
use serde_repr::{Serialize_repr, Deserialize_repr};
#[derive(Serialize_repr, Deserialize_repr, Clone, Debug)]
#[repr(u16)]
/// The request's body type
pub enum BodyType {
/// Send request body as application/x-www-form-urlencoded
Form = 1,
/// Send request body (which is a path to a file) as application/octet-stream
File,
/// Detects the body type automatically
/// - if the body is a byte array, send is as bytes (application/octet-stream)
/// - if the body is an object or array, send it as JSON (application/json with UTF-8 charset)
/// - if the body is a string, send it as text (text/plain with UTF-8 charset)
Auto
}
#[derive(Serialize_repr, Deserialize_repr, Clone, Debug)]
#[repr(u16)]
/// The request's response type
pub enum ResponseType {
/// Read the response as JSON
Json = 1,
/// Read the response as text
Text,
/// Read the response as binary
Binary
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
/// The configuration object of an HTTP request
pub struct HttpRequestOptions {
/// The request method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT or TRACE)
pub method: String,
/// The request URL
pub url: String,
/// The request query params
pub params: Option<HashMap<String, Value>>,
/// The request headers
pub headers: Option<HashMap<String, Value>>,
/// The request body
pub body: Option<Value>,
/// Whether to follow redirects or not
pub follow_redirects: Option<bool>,
/// Max number of redirections to follow
pub max_redirections: Option<u32>,
/// Connect timeout for the request
pub connect_timeout: Option<u64>,
/// Read timeout for the request
pub read_timeout: Option<u64>,
/// Timeout for the whole request
pub timeout: Option<u64>,
/// Whether the request will announce that it accepts compression
pub allow_compression: Option<bool>,
/// The body type (defaults to Auto)
pub body_type: Option<BodyType>,
/// The response type (defaults to Json)
pub response_type: Option<ResponseType>,
}
/// Executes an HTTP request
///
/// The response will be transformed to String,
/// If reading the response as binary, the byte array will be serialized using serde_json
pub fn make_request(options: HttpRequestOptions) -> crate::Result<String> {
let method = Method::from_bytes(options.method.to_uppercase().as_bytes())?;
let mut builder = RequestBuilder::new(method, options.url);
if let Some(params) = options.params {
for (param, param_value) in params.iter() {
builder = builder.param(param, param_value);
}
}
if let Some(headers) = options.headers {
for (header, header_value) in headers.iter() {
builder = builder.header(HeaderName::from_bytes(header.as_bytes())?, format!("{}", header_value));
}
}
if let Some(follow_redirects) = options.follow_redirects {
builder = builder.follow_redirects(follow_redirects);
}
if let Some(max_redirections) = options.max_redirections {
builder = builder.max_redirections(max_redirections);
}
if let Some(connect_timeout) = options.connect_timeout {
builder = builder.connect_timeout(Duration::from_secs(connect_timeout));
}
if let Some(read_timeout) = options.read_timeout {
builder = builder.read_timeout(Duration::from_secs(read_timeout));
}
if let Some(timeout) = options.timeout {
builder = builder.timeout(Duration::from_secs(timeout));
}
if let Some(allow_compression) = options.allow_compression {
builder = builder.allow_compression(allow_compression);
}
builder = builder.danger_accept_invalid_certs(true).danger_accept_invalid_hostnames(true);
let response = if let Some(body) = options.body {
match options.body_type.unwrap_or(BodyType::Auto) {
BodyType::Form => builder.form(&body)?.send(),
BodyType::File => {
if let Some(path) = body.as_str() {
builder.file(File::open(path)?).send()
} else {
return Err(crate::Error::from("Body must be the path to the file"));
}
},
BodyType::Auto => {
if body.is_object() {
builder.json(&body)?.send()
} else if let Some(text) = body.as_str() {
builder.text(&text).send()
} else if body.is_array() {
let u: Result<Vec<u8>, _> = serde_json::from_value(body.clone());
match u {
Ok(vec) => builder.bytes(&vec).send(),
Err(_) => builder.json(&body)?.send()
}
} else {
builder.send()
}
}
}
} else { builder.send() };
let response = response?;
if response.is_success() {
let response_data = match options.response_type.unwrap_or(ResponseType::Json) {
ResponseType::Json => response.json()?,
ResponseType::Text => response.text()?,
ResponseType::Binary => serde_json::to_string(&response.bytes()?)?
};
Ok(response_data)
} else {
Err(crate::Error::from(response.status().as_str()))
}
}

View File

@ -11,6 +11,7 @@ pub mod version;
pub mod tcp; pub mod tcp;
pub mod dialog; pub mod dialog;
pub mod path; pub mod path;
pub mod http;
pub use tauri_utils::*; pub use tauri_utils::*;
@ -22,6 +23,10 @@ error_chain! {
ZipError(::zip::result::ZipError); ZipError(::zip::result::ZipError);
SemVer(::semver::SemVerError); SemVer(::semver::SemVerError);
Platform(::tauri_utils::Error); Platform(::tauri_utils::Error);
Json(::serde_json::Error);
Http(::attohttpc::Error);
HttpMethod(::http::method::InvalidMethod);
HttpHeaderName(::http::header::InvalidHeaderName);
} }
errors { errors {
Extract(t: String) { Extract(t: String) {

View File

@ -0,0 +1,23 @@
const methodSelect = document.getElementById('request-method')
const requestUrlInput = document.getElementById('request-url')
const requestBodyInput = document.getElementById('request-body')
document.getElementById('make-request').addEventListener('click', function () {
const method = methodSelect.value || 'GET'
const url = requestUrlInput.value || ''
const options = {
url: url,
method: method
}
let body = requestBodyInput.value || ''
if ((body.startsWith('{') && body.endsWith('}')) || (body.startsWith('[') && body.endsWith(']'))) {
body = JSON.parse(body)
} else if (body.startsWith('/') || body.match(/\S:\//g)) {
options.bodyAsFile = true
}
options.body = body
window.tauri.httpRequest(options).then(registerResponse).catch(registerResponse)
})

View File

@ -41,6 +41,19 @@
<button id="save-dialog">Open save dialog</button> <button id="save-dialog">Open save dialog</button>
</div> </div>
<div style="margin-top: 24px">
<select id="request-method">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="PATCH">PATCH</option>
<option value="DELETE">DELETE</option>
</select>
<input id="request-url" placeholder="Type the request URL...">
<textarea id="request-body" placeholder="Request body"></textarea>
<button id="make-request">Make request</button>
</div>
<div id="response"></div> <div id="response"></div>
<script> <script>
@ -76,5 +89,6 @@
<script src="fs.js"></script> <script src="fs.js"></script>
<script src="window.js"></script> <script src="window.js"></script>
<script src="dialog.js"></script> <script src="dialog.js"></script>
<script src="http.js"></script>
</body> </body>
</html> </html>

View File

@ -100,6 +100,9 @@ var Dir = {
} }
function camelToKebab (string) {
return string.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase()
}
/** /**
* @name return __whitelistWarning * @name return __whitelistWarning
* @description Present a stylish warning to the developer that their API * @description Present a stylish warning to the developer that their API
@ -108,7 +111,7 @@ var Dir = {
* @private * @private
*/ */
var __whitelistWarning = function (func) { var __whitelistWarning = function (func) {
console.warn('%c[Tauri] Danger \ntauri.' + func + ' not whitelisted 💣\n%c\nAdd to tauri.conf.json: \n\ntauri: \n whitelist: { \n ' + func + ': true \n\nReference: https://github.com/tauri-apps/tauri/wiki' + func, 'background: red; color: white; font-weight: 800; padding: 2px; font-size:1.5em', ' ') console.warn('%c[Tauri] Danger \ntauri.' + func + ' not whitelisted 💣\n%c\nAdd to tauri.conf.json: \n\ntauri: \n whitelist: { \n ' + camelToKebab(func) + ': true \n\nReference: https://github.com/tauri-apps/tauri/wiki' + func, 'background: red; color: white; font-weight: 800; padding: 2px; font-size:1.5em', ' ')
return __reject() return __reject()
} }
@ -417,8 +420,8 @@ window.tauri = {
return this.promisified({ return this.promisified({
cmd: 'renameFile', cmd: 'renameFile',
old_path: oldPath, oldPath: oldPath,
new_path: newPath, newPath: newPath,
options: options options: options
}); });
@ -531,13 +534,43 @@ window.tauri = {
}, },
loadAsset: function loadAsset(assetName, assetType) {
return this.promisified({ /**
cmd: 'loadAsset', * @name httpRequest
asset: assetName, * @description Makes an HTTP request
asset_type: assetType || 'unknown' * @param {Object} options
}) * @param {String} options.method GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT or TRACE
} * @param {String} options.url the request URL
* @param {Object} [options.headers] the request headers
* @param {Object} [options.params] the request query params
* @param {Object|String|Binary} [options.body] the request body
* @param {Boolean} followRedirects whether to follow redirects or not
* @param {Number} maxRedirections max number of redirections
* @param {Number} connectTimeout request connect timeout
* @param {Number} readTimeout request read timeout
* @param {Number} timeout request timeout
* @param {Boolean} allowCompression
* @param {Boolean} bodyAsForm send the body as application/x-www-form-urlencoded
* @param {Boolean} bodyAsFile send the body as a file (body must be the path to the file)
* @returns {Promise<any>}
*/
httpRequest: function httpRequest(options) {
return this.promisified({
cmd: 'httpRequest',
options: options
});
},
loadAsset: function loadAsset(assetName, assetType) {
return this.promisified({
cmd: 'loadAsset',
asset: assetName,
assetType: assetType || 'unknown'
})
}
}; };
// init tauri API // init tauri API
@ -591,4 +624,4 @@ if (document.readyState === 'complete' || document.readyState === 'interactive')
__openLinks() __openLinks()
}, true) }, true)
} }
</script> <div> <button id="log">Call Log API</button> <button id="request">Call Request (async) API</button> <button id="event">Send event to Rust</button> </div> <div style="margin-top:24px"> <select id="dir"> <option value="">None</option> </select> <input id="path-to-read" placeholder="Type the path to read..."> <button id="read">Read</button> </div> <div style="margin-top:24px"> <input id="url" value="https://tauri.studio"> <button id="open-url">Open URL</button> </div> <div style="margin-top:24px"> <input id="title" value="Awesome Tauri Example!"> <button id="set-title">Set title</button> </div> <div style="margin-top:24px"> <input id="dialog-default-path" placeholder="Default path"> <input id="dialog-filter" placeholder="Extensions filter"> <div> <input type="checkbox" id="dialog-multiple"> <label>Multiple</label> </div> <div> <input type="checkbox" id="dialog-directory"> <label>Directory</label> </div> <button id="open-dialog">Open dialog</button> <button id="save-dialog">Open save dialog</button> </div> <div id="response"></div> <script>function registerResponse(e){document.getElementById("response").innerHTML="object"==typeof e?JSON.stringify(e):e}function addClickEnterHandler(e,n,t){e.addEventListener("click",t),n.addEventListener("keyup",function(e){13===e.keyCode&&t()})}window.tauri.listen("rust-event",function(e){document.getElementById("response").innerHTML=JSON.stringify(e)});var dirSelect=document.getElementById("dir");for(var key in window.tauri.Dir){var value=window.tauri.Dir[key],opt=document.createElement("option");opt.value=value,opt.innerHTML=key,dirSelect.appendChild(opt)}</script> <script>document.getElementById("log").addEventListener("click",function(){window.tauri.invoke({cmd:"logOperation",event:"tauri-click",payload:"this payload is optional because we used Option in Rust"})}),document.getElementById("request").addEventListener("click",function(){window.tauri.promisified({cmd:"performRequest",endpoint:"dummy endpoint arg",body:{id:5,name:"test"}}).then(registerResponse).catch(registerResponse)}),document.getElementById("event").addEventListener("click",function(){window.tauri.emit("js-event","this is the payload string")});</script> <script>var dirSelect=document.getElementById("dir");function getDir(){return dirSelect.value?parseInt(dir.value):null}function arrayBufferToBase64(e,n){var t=new Blob([e],{type:"application/octet-binary"}),r=new FileReader;r.onload=function(e){var t=e.target.result;n(t.substr(t.indexOf(",")+1))},r.readAsDataURL(t)}var pathInput=document.getElementById("path-to-read");addClickEnterHandler(document.getElementById("read"),pathInput,function(){var r=pathInput.value,a=r.match(/\S+\.\S+$/g),e={dir:getDir()};(a?window.tauri.readBinaryFile(r,e):window.tauri.readDir(r,e)).then(function(e){if(a)if(r.includes(".png")||r.includes(".jpg"))arrayBufferToBase64(new Uint8Array(e),function(e){registerResponse('<img src="'+("data:image/png;base64,"+e)+'"></img>')});else{var t=String.fromCharCode.apply(null,e);registerResponse('<textarea id="file-response" style="height: 400px"></textarea><button id="file-save">Save</button>');var n=document.getElementById("file-response");n.value=t,document.getElementById("file-save").addEventListener("click",function(){window.tauri.writeFile({file:r,contents:n.value},{dir:getDir()}).catch(registerResponse)})}else registerResponse(e)}).catch(registerResponse)});</script> <script>var urlInput=document.getElementById("url");addClickEnterHandler(document.getElementById("open-url"),urlInput,function(){window.tauri.open(urlInput.value)});var titleInput=document.getElementById("title");addClickEnterHandler(document.getElementById("set-title"),titleInput,function(){window.tauri.setTitle(titleInput.value)});</script> <script>var defaultPathInput=document.getElementById("dialog-default-path"),filterInput=document.getElementById("dialog-filter"),multipleInput=document.getElementById("dialog-multiple"),directoryInput=document.getElementById("dialog-directory");document.getElementById("open-dialog").addEventListener("click",function(){window.tauri.openDialog({defaultPath:defaultPathInput.value||null,filter:filterInput.value||null,multiple:multipleInput.checked,directory:directoryInput.checked}).then(registerResponse).catch(registerResponse)}),document.getElementById("save-dialog").addEventListener("click",function(){window.tauri.saveDialog({defaultPath:defaultPathInput.value||null,filter:filterInput.value||null}).then(registerResponse).catch(registerResponse)});</script> </body></html> </script> <div> <button id="log">Call Log API</button> <button id="request">Call Request (async) API</button> <button id="event">Send event to Rust</button> </div> <div style="margin-top:24px"> <select id="dir"> <option value="">None</option> </select> <input id="path-to-read" placeholder="Type the path to read..."> <button id="read">Read</button> </div> <div style="margin-top:24px"> <input id="url" value="https://tauri.studio"> <button id="open-url">Open URL</button> </div> <div style="margin-top:24px"> <input id="title" value="Awesome Tauri Example!"> <button id="set-title">Set title</button> </div> <div style="margin-top:24px"> <input id="dialog-default-path" placeholder="Default path"> <input id="dialog-filter" placeholder="Extensions filter"> <div> <input type="checkbox" id="dialog-multiple"> <label>Multiple</label> </div> <div> <input type="checkbox" id="dialog-directory"> <label>Directory</label> </div> <button id="open-dialog">Open dialog</button> <button id="save-dialog">Open save dialog</button> </div> <div style="margin-top:24px"> <select id="request-method"> <option value="GET">GET</option> <option value="POST">POST</option> <option value="PUT">PUT</option> <option value="PATCH">PATCH</option> <option value="DELETE">DELETE</option> </select> <input id="request-url" placeholder="Type the request URL..."> <textarea id="request-body" placeholder="Request body"></textarea> <button id="make-request">Make request</button> </div> <div id="response"></div> <script>function registerResponse(e){document.getElementById("response").innerHTML="object"==typeof e?JSON.stringify(e):e}function addClickEnterHandler(e,n,t){e.addEventListener("click",t),n.addEventListener("keyup",function(e){13===e.keyCode&&t()})}window.tauri.listen("rust-event",function(e){document.getElementById("response").innerHTML=JSON.stringify(e)});var dirSelect=document.getElementById("dir");for(var key in window.tauri.Dir){var value=window.tauri.Dir[key],opt=document.createElement("option");opt.value=value,opt.innerHTML=key,dirSelect.appendChild(opt)}</script> <script>document.getElementById("log").addEventListener("click",function(){window.tauri.invoke({cmd:"logOperation",event:"tauri-click",payload:"this payload is optional because we used Option in Rust"})}),document.getElementById("request").addEventListener("click",function(){window.tauri.promisified({cmd:"performRequest",endpoint:"dummy endpoint arg",body:{id:5,name:"test"}}).then(registerResponse).catch(registerResponse)}),document.getElementById("event").addEventListener("click",function(){window.tauri.emit("js-event","this is the payload string")});</script> <script>var dirSelect=document.getElementById("dir");function getDir(){return dirSelect.value?parseInt(dir.value):null}function arrayBufferToBase64(e,n){var t=new Blob([e],{type:"application/octet-binary"}),r=new FileReader;r.onload=function(e){var t=e.target.result;n(t.substr(t.indexOf(",")+1))},r.readAsDataURL(t)}var pathInput=document.getElementById("path-to-read");addClickEnterHandler(document.getElementById("read"),pathInput,function(){var r=pathInput.value,a=r.match(/\S+\.\S+$/g),e={dir:getDir()};(a?window.tauri.readBinaryFile(r,e):window.tauri.readDir(r,e)).then(function(e){if(a)if(r.includes(".png")||r.includes(".jpg"))arrayBufferToBase64(new Uint8Array(e),function(e){registerResponse('<img src="'+("data:image/png;base64,"+e)+'"></img>')});else{var t=String.fromCharCode.apply(null,e);registerResponse('<textarea id="file-response" style="height: 400px"></textarea><button id="file-save">Save</button>');var n=document.getElementById("file-response");n.value=t,document.getElementById("file-save").addEventListener("click",function(){window.tauri.writeFile({file:r,contents:n.value},{dir:getDir()}).catch(registerResponse)})}else registerResponse(e)}).catch(registerResponse)});</script> <script>var urlInput=document.getElementById("url");addClickEnterHandler(document.getElementById("open-url"),urlInput,function(){window.tauri.open(urlInput.value)});var titleInput=document.getElementById("title");addClickEnterHandler(document.getElementById("set-title"),titleInput,function(){window.tauri.setTitle(titleInput.value)});</script> <script>var defaultPathInput=document.getElementById("dialog-default-path"),filterInput=document.getElementById("dialog-filter"),multipleInput=document.getElementById("dialog-multiple"),directoryInput=document.getElementById("dialog-directory");document.getElementById("open-dialog").addEventListener("click",function(){window.tauri.openDialog({defaultPath:defaultPathInput.value||null,filter:filterInput.value||null,multiple:multipleInput.checked,directory:directoryInput.checked}).then(registerResponse).catch(registerResponse)}),document.getElementById("save-dialog").addEventListener("click",function(){window.tauri.saveDialog({defaultPath:defaultPathInput.value||null,filter:filterInput.value||null}).then(registerResponse).catch(registerResponse)});</script> <script></script> </body></html>

View File

@ -3,12 +3,20 @@ use serde::Deserialize;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct RequestBody { pub struct RequestBody {
id: i32, id: i32,
name: String name: String,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(tag = "cmd", rename_all = "camelCase")] #[serde(tag = "cmd", rename_all = "camelCase")]
pub enum Cmd { pub enum Cmd {
LogOperation { event: String, payload: Option<String> }, LogOperation {
PerformRequest { endpoint: String, body: RequestBody, callback: String, error: String }, event: String,
payload: Option<String>,
},
PerformRequest {
endpoint: String,
body: RequestBody,
callback: String,
error: String,
},
} }

View File

@ -9,7 +9,7 @@ use serde::Serialize;
#[derive(Serialize)] #[derive(Serialize)]
struct Reply { struct Reply {
data: String data: String,
} }
fn main() { fn main() {
@ -32,15 +32,18 @@ fn main() {
.invoke_handler(|_webview, arg| { .invoke_handler(|_webview, arg| {
use cmd::Cmd::*; use cmd::Cmd::*;
match serde_json::from_str(arg) { match serde_json::from_str(arg) {
Err(e) => { Err(e) => Err(e.to_string()),
Err(e.to_string())
}
Ok(command) => { Ok(command) => {
match command { match command {
LogOperation { event, payload } => { LogOperation { event, payload } => {
println!("{} {:?}", event, payload); println!("{} {:?}", event, payload);
}, }
PerformRequest { endpoint, body, callback, error } => { PerformRequest {
endpoint,
body,
callback,
error,
} => {
// tauri::execute_promise is a helper for APIs that uses the tauri.promisified JS function // tauri::execute_promise is a helper for APIs that uses the tauri.promisified JS function
// so you can easily communicate between JS and Rust with promises // so you can easily communicate between JS and Rust with promises
tauri::execute_promise( tauri::execute_promise(
@ -54,9 +57,9 @@ fn main() {
Ok("{ key: 'response', value: [{ id: 3 }] }".to_string()) Ok("{ key: 'response', value: [{ id: 3 }] }".to_string())
}, },
callback, callback,
error error,
) )
}, }
} }
Ok(()) Ok(())
} }

View File

@ -16,10 +16,13 @@ impl App {
runner::run(&mut self).expect("Failed to build webview"); runner::run(&mut self).expect("Failed to build webview");
} }
pub(crate) fn run_invoke_handler(&mut self, webview: &mut WebView<'_, ()>, arg: &str) -> Result<bool, String> { pub(crate) fn run_invoke_handler(
&mut self,
webview: &mut WebView<'_, ()>,
arg: &str,
) -> Result<bool, String> {
if let Some(ref mut invoke_handler) = self.invoke_handler { if let Some(ref mut invoke_handler) = self.invoke_handler {
invoke_handler(webview, arg) invoke_handler(webview, arg).map(|_| true)
.map(|_| true)
} else { } else {
Ok(false) Ok(false)
} }
@ -40,7 +43,7 @@ impl App {
pub struct AppBuilder { pub struct AppBuilder {
invoke_handler: Option<InvokeHandler>, invoke_handler: Option<InvokeHandler>,
setup: Option<Setup>, setup: Option<Setup>,
splashscreen_html: Option<String> splashscreen_html: Option<String>,
} }
impl AppBuilder { impl AppBuilder {

View File

@ -4,9 +4,9 @@ use std::{fs::read_to_string, path::Path, process::Stdio, thread::spawn};
use web_view::{builder, Content, WebView}; use web_view::{builder, Content, WebView};
use super::App; use super::App;
use crate::config::{get, Config};
#[cfg(feature = "embedded-server")] #[cfg(feature = "embedded-server")]
use crate::api::tcp::{get_available_port, port_is_available}; use crate::api::tcp::{get_available_port, port_is_available};
use crate::config::{get, Config};
// Main entry point function for running the Webview // Main entry point function for running the Webview
pub(crate) fn run(application: &mut App) -> crate::Result<()> { pub(crate) fn run(application: &mut App) -> crate::Result<()> {
@ -32,7 +32,12 @@ pub(crate) fn run(application: &mut App) -> crate::Result<()> {
config, config,
main_content, main_content,
if application.splashscreen_html().is_some() { if application.splashscreen_html().is_some() {
Some(Content::Html(application.splashscreen_html().expect("failed to get splashscreen_html").to_string())) Some(Content::Html(
application
.splashscreen_html()
.expect("failed to get splashscreen_html")
.to_string(),
))
} else { } else {
None None
}, },
@ -153,7 +158,7 @@ fn build_webview(
application: &mut App, application: &mut App,
config: Config, config: Config,
content: Content<String>, content: Content<String>,
splashscreen_content: Option<Content<String>> splashscreen_content: Option<Content<String>>,
) -> crate::Result<WebView<'_, ()>> { ) -> crate::Result<WebView<'_, ()>> {
let content_clone = match content { let content_clone = match content {
Content::Html(ref html) => Content::Html(html.clone()), Content::Html(ref html) => Content::Html(html.clone()),
@ -201,16 +206,18 @@ fn build_webview(
Some(e.replace("'", "\\'")) Some(e.replace("'", "\\'"))
} else { } else {
let handled = handled_by_app.expect("failed to check if the invoke was handled"); let handled = handled_by_app.expect("failed to check if the invoke was handled");
if handled { None } else { Some(tauri_handle_error_str) } if handled {
None
} else {
Some(tauri_handle_error_str)
}
}; };
} else { } else {
handler_error = Some(tauri_handle_error_str); handler_error = Some(tauri_handle_error_str);
} }
if let Some(handler_error_message) = handler_error { if let Some(handler_error_message) = handler_error {
webview.eval( webview.eval(&get_api_error_message(arg, handler_error_message))?;
&get_api_error_message(arg, handler_error_message)
)?;
} }
} }
} }
@ -229,8 +236,8 @@ fn build_webview(
if has_splashscreen { if has_splashscreen {
// inject the tauri.js entry point // inject the tauri.js entry point
webview webview
.handle() .handle()
.dispatch(|_webview| _webview.eval(include_str!(concat!(env!("TAURI_DIR"), "/tauri.js"))))?; .dispatch(|_webview| _webview.eval(include_str!(concat!(env!("TAURI_DIR"), "/tauri.js"))))?;
} }
Ok(webview) Ok(webview)

View File

@ -1,8 +1,8 @@
mod cmd; mod cmd;
mod salt;
#[allow(dead_code)]
mod file_system;
mod dialog; mod dialog;
mod file_system;
mod http;
mod salt;
#[cfg(any(feature = "embedded-server", feature = "no-server"))] #[cfg(any(feature = "embedded-server", feature = "no-server"))]
use std::path::PathBuf; use std::path::PathBuf;
@ -149,7 +149,7 @@ pub(crate) fn handle<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> cra
OpenDialog { OpenDialog {
options, options,
callback, callback,
error error,
} => { } => {
dialog::open(webview, options, callback, error); dialog::open(webview, options, callback, error);
} }
@ -161,7 +161,15 @@ pub(crate) fn handle<T: 'static>(webview: &mut WebView<'_, T>, arg: &str) -> cra
} => { } => {
dialog::save(webview, options, callback, error); dialog::save(webview, options, callback, error);
} }
#[cfg(any(feature = "embedded-server", feature = "no-server"))] #[cfg(any(feature = "all-api", feature = "http-request"))]
HttpRequest {
options,
callback,
error,
} => {
http::make_request(webview, options, callback, error);
}
#[cfg(any(feature = "embedded-server", feature = "no-server"))]
LoadAsset { LoadAsset {
asset, asset,
asset_type, asset_type,

View File

@ -1,5 +1,6 @@
use serde::Deserialize;
use crate::api::path::BaseDirectory; use crate::api::path::BaseDirectory;
use serde::Deserialize;
use tauri_api::http::HttpRequestOptions;
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct DirOperationOptions { pub struct DirOperationOptions {
@ -14,6 +15,7 @@ pub struct FileOperationOptions {
} }
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OpenDialogOptions { pub struct OpenDialogOptions {
pub filter: Option<String>, pub filter: Option<String>,
#[serde(default)] #[serde(default)]
@ -24,6 +26,7 @@ pub struct OpenDialogOptions {
} }
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SaveDialogOptions { pub struct SaveDialogOptions {
pub filter: Option<String>, pub filter: Option<String>,
pub default_path: Option<String>, pub default_path: Option<String>,
@ -91,6 +94,7 @@ pub enum Cmd {
callback: String, callback: String,
error: String, error: String,
}, },
#[serde(rename_all = "camelCase")]
#[cfg(any(feature = "all-api", feature = "rename-file"))] #[cfg(any(feature = "all-api", feature = "rename-file"))]
RenameFile { RenameFile {
old_path: String, old_path: String,
@ -142,6 +146,13 @@ pub enum Cmd {
callback: String, callback: String,
error: String, error: String,
}, },
#[cfg(any(feature = "all-api", feature = "http-request"))]
HttpRequest {
options: HttpRequestOptions,
callback: String,
error: String,
},
#[serde(rename_all = "camelCase")]
#[cfg(any(feature = "embedded-server", feature = "no-server"))] #[cfg(any(feature = "embedded-server", feature = "no-server"))]
LoadAsset { LoadAsset {
asset: String, asset: String,

View File

@ -1,12 +1,12 @@
use crate::api::dialog::{select, select_multiple, save_file, pick_folder, Response};
use super::cmd::{OpenDialogOptions, SaveDialogOptions}; use super::cmd::{OpenDialogOptions, SaveDialogOptions};
use crate::api::dialog::{pick_folder, save_file, select, select_multiple, Response};
use web_view::WebView; use web_view::WebView;
fn map_response(response: Response) -> String { fn map_response(response: Response) -> String {
match response { match response {
Response::Okay(path) => format!(r#""{}""#, path).replace("\\", "\\\\"), Response::Okay(path) => format!(r#""{}""#, path).replace("\\", "\\\\"),
Response::OkayMultiple(paths) => format!("{:?}", paths), Response::OkayMultiple(paths) => format!("{:?}", paths),
Response::Cancel => panic!("unexpected response type") Response::Cancel => panic!("unexpected response type"),
} }
} }

View File

@ -52,14 +52,15 @@ pub fn copy_file<T: 'static>(
webview, webview,
move || { move || {
let (src, dest) = match options.and_then(|o| o.dir) { let (src, dest) = match options.and_then(|o| o.dir) {
Some(dir) => { Some(dir) => (
(resolve_path(source, Some(dir.clone()))?, resolve_path(destination, Some(dir))?) resolve_path(source, Some(dir.clone()))?,
} resolve_path(destination, Some(dir))?,
None => (source, destination) ),
None => (source, destination),
}; };
fs::copy(src, dest) fs::copy(src, dest)
.map_err(|e| crate::ErrorKind::FileSystem(e.to_string()).into()) .map_err(|e| crate::ErrorKind::FileSystem(e.to_string()).into())
.map(|_| "".to_string()) .map(|_| "".to_string())
}, },
callback, callback,
error, error,
@ -90,7 +91,7 @@ pub fn create_dir<T: 'static>(
response response
.map_err(|e| crate::ErrorKind::FileSystem(e.to_string()).into()) .map_err(|e| crate::ErrorKind::FileSystem(e.to_string()).into())
.map(|_| "".to_string()) .map(|_| "".to_string())
}, },
callback, callback,
error, error,
@ -160,10 +161,11 @@ pub fn rename_file<T: 'static>(
webview, webview,
move || { move || {
let (old, new) = match options.and_then(|o| o.dir) { let (old, new) = match options.and_then(|o| o.dir) {
Some(dir) => { Some(dir) => (
(resolve_path(old_path, Some(dir.clone()))?, resolve_path(new_path, Some(dir))?) resolve_path(old_path, Some(dir.clone()))?,
} resolve_path(new_path, Some(dir))?,
None => (old_path, new_path) ),
None => (old_path, new_path),
}; };
fs::rename(old, new) fs::rename(old, new)
.map_err(|e| crate::ErrorKind::FileSystem(e.to_string()).into()) .map_err(|e| crate::ErrorKind::FileSystem(e.to_string()).into())
@ -210,10 +212,7 @@ pub fn read_text_file<T: 'static>(
move || { move || {
file::read_string(resolve_path(path, options.and_then(|o| o.dir))?) file::read_string(resolve_path(path, options.and_then(|o| o.dir))?)
.map_err(|e| crate::ErrorKind::FileSystem(e.to_string()).into()) .map_err(|e| crate::ErrorKind::FileSystem(e.to_string()).into())
.and_then(|f| { .and_then(|f| serde_json::to_string(&f).map_err(|err| err.into()))
serde_json::to_string(&f)
.map_err(|err| err.into())
})
}, },
callback, callback,
error, error,
@ -232,10 +231,7 @@ pub fn read_binary_file<T: 'static>(
move || { move || {
file::read_binary(resolve_path(path, options.and_then(|o| o.dir))?) file::read_binary(resolve_path(path, options.and_then(|o| o.dir))?)
.map_err(|e| crate::ErrorKind::FileSystem(e.to_string()).into()) .map_err(|e| crate::ErrorKind::FileSystem(e.to_string()).into())
.and_then(|f| { .and_then(|f| serde_json::to_string(&f).map_err(|err| err.into()))
serde_json::to_string(&f)
.map_err(|err| err.into())
})
}, },
callback, callback,
error, error,

View File

@ -0,0 +1,27 @@
use tauri_api::http::{make_request as request, HttpRequestOptions, ResponseType};
use web_view::WebView;
/// Makes a HTTP request and resolves the response to the webview
pub fn make_request<T: 'static>(
webview: &mut WebView<'_, T>,
options: HttpRequestOptions,
callback: String,
error: String,
) {
crate::execute_promise(
webview,
move || {
let response_type = options.response_type.clone();
request(options)
.map_err(|e| crate::ErrorKind::Http(e.to_string()).into())
.map(|response| {
match response_type.unwrap_or(ResponseType::Json) {
ResponseType::Text => format!(r#""{}""#, response),
_ => response
}
})
},
callback,
error,
);
}

View File

@ -50,6 +50,10 @@ error_chain! {
description("FileSystem Error") description("FileSystem Error")
display("FileSystem Error: '{}'", t) display("FileSystem Error: '{}'", t)
} }
Http(t: String) {
description("Http Error")
display("Http Error: '{}'", t)
}
} }
} }