refactor: custom protocol (#2503)

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
This commit is contained in:
david 2021-08-23 07:09:23 -07:00 committed by GitHub
parent 994b5325dd
commit 539e4489e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 4268 additions and 183 deletions

View File

@ -0,0 +1,6 @@
---
"tauri": patch
"tauri-runtime": patch
---
**Breaking change:** Removed `register_uri_scheme_protocol` from the `WebviewAttibutes` struct and renamed `register_global_uri_scheme_protocol` to `register_uri_scheme_protocol` on the `Builder` struct, which now takes a `Fn(&AppHandle, &http::Request) -> http::Response` closure.

View File

@ -0,0 +1,7 @@
---
"tauri": minor
"tauri-runtime": minor
"tauri-runtime-wry": minor
---
Migrate to latest custom protocol allowing `Partial content` streaming and Header parsing.

1
.gitignore vendored
View File

@ -93,3 +93,4 @@ __handlers__/
# benches
gh-pages
test_video.mp4

View File

@ -5,6 +5,10 @@
//! The [`wry`] Tauri [`Runtime`].
use tauri_runtime::{
http::{
Request as HttpRequest, RequestParts as HttpRequestParts, Response as HttpResponse,
ResponseParts as HttpResponseParts,
},
menu::{CustomMenuItem, Menu, MenuEntry, MenuHash, MenuItem, MenuUpdate, Submenu},
monitor::Monitor,
webview::{
@ -54,6 +58,10 @@ use wry::{
monitor::MonitorHandle,
window::{Fullscreen, Icon as WindowIcon, UserAttentionType as WryUserAttentionType},
},
http::{
Request as WryHttpRequest, RequestParts as WryRequestParts, Response as WryHttpResponse,
ResponseParts as WryResponseParts,
},
webview::{
FileDropEvent as WryFileDropEvent, RpcRequest as WryRpcRequest, RpcResponse, WebContext,
WebView, WebViewBuilder,
@ -95,9 +103,6 @@ mod system_tray;
#[cfg(feature = "system-tray")]
use system_tray::*;
mod mime_type;
use mime_type::MimeType;
type WebContextStore = Mutex<HashMap<Option<PathBuf>, WebContext>>;
// window
type WindowEventHandler = Box<dyn Fn(&WindowEvent) + Send>;
@ -152,6 +157,72 @@ struct EventLoopContext {
proxy: EventLoopProxy<Message>,
}
struct HttpRequestPartsWrapper(HttpRequestParts);
impl From<HttpRequestPartsWrapper> for HttpRequestParts {
fn from(parts: HttpRequestPartsWrapper) -> Self {
Self {
method: parts.0.method,
uri: parts.0.uri,
headers: parts.0.headers,
}
}
}
impl From<HttpRequestParts> for HttpRequestPartsWrapper {
fn from(request: HttpRequestParts) -> Self {
Self(HttpRequestParts {
method: request.method,
uri: request.uri,
headers: request.headers,
})
}
}
impl From<WryRequestParts> for HttpRequestPartsWrapper {
fn from(request: WryRequestParts) -> Self {
Self(HttpRequestParts {
method: request.method,
uri: request.uri,
headers: request.headers,
})
}
}
struct HttpRequestWrapper(HttpRequest);
impl From<&WryHttpRequest> for HttpRequestWrapper {
fn from(req: &WryHttpRequest) -> Self {
Self(HttpRequest {
body: req.body.clone(),
head: HttpRequestPartsWrapper::from(req.head.clone()).0,
})
}
}
// response
struct HttpResponsePartsWrapper(WryResponseParts);
impl From<HttpResponseParts> for HttpResponsePartsWrapper {
fn from(response: HttpResponseParts) -> Self {
Self(WryResponseParts {
mimetype: response.mimetype,
status: response.status,
version: response.version,
headers: response.headers,
})
}
}
struct HttpResponseWrapper(WryHttpResponse);
impl From<HttpResponse> for HttpResponseWrapper {
fn from(response: HttpResponse) -> Self {
Self(WryHttpResponse {
body: response.body,
head: HttpResponsePartsWrapper::from(response.head).0,
})
}
}
pub struct MenuItemAttributesWrapper<'a>(pub WryMenuItemAttributes<'a>);
impl<'a> From<&'a CustomMenuItem> for MenuItemAttributesWrapper<'a> {
@ -2327,6 +2398,7 @@ fn create_webview(
#[allow(unused_mut)]
let PendingWindow {
webview_attributes,
uri_scheme_protocols,
mut window_builder,
rpc_handler,
file_drop_handler,
@ -2375,13 +2447,10 @@ fn create_webview(
handler,
));
}
for (scheme, protocol) in webview_attributes.uri_scheme_protocols {
webview_builder = webview_builder.with_custom_protocol(scheme, move |url| {
protocol(url)
.map(|data| {
let mime_type = MimeType::parse(&data, url);
(data, mime_type)
})
for (scheme, protocol) in uri_scheme_protocols {
webview_builder = webview_builder.with_custom_protocol(scheme, move |wry_request| {
protocol(&HttpRequestWrapper::from(wry_request).0)
.map(|tauri_response| HttpResponseWrapper::from(tauri_response).0)
.map_err(|_| wry::Error::InitScriptError)
});
}
@ -2409,7 +2478,7 @@ fn create_webview(
.build()
.map_err(|e| Error::CreateWebview(Box::new(e)))?
} else {
let mut context = WebContext::new(webview_attributes.data_directory.clone());
let mut context = WebContext::new(webview_attributes.data_directory);
webview_builder
.with_web_context(&mut context)
.build()

View File

@ -27,6 +27,9 @@ serde_json = "1.0"
thiserror = "1.0"
tauri-utils = { version = "1.0.0-beta.3", path = "../tauri-utils" }
uuid = { version = "0.8.2", features = [ "v4" ] }
http = "0.2.4"
http-range = "0.1.4"
infer = "0.4"
[target."cfg(windows)".dependencies]
winapi = "0.3"

View File

@ -7,7 +7,7 @@ use std::fmt;
const MIMETYPE_PLAIN: &str = "text/plain";
/// [Web Compatible MimeTypes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#important_mime_types_for_web_developers)
pub(crate) enum MimeType {
pub enum MimeType {
Css,
Csv,
Html,
@ -18,6 +18,7 @@ pub(crate) enum MimeType {
OctetStream,
Rtf,
Svg,
Mp4,
}
impl std::fmt::Display for MimeType {
@ -33,6 +34,7 @@ impl std::fmt::Display for MimeType {
MimeType::OctetStream => "application/octet-stream",
MimeType::Rtf => "application/rtf",
MimeType::Svg => "image/svg+xml",
MimeType::Mp4 => "video/mp4",
};
write!(f, "{}", mime)
}
@ -53,6 +55,7 @@ impl MimeType {
Some("jsonld") => Self::Jsonld,
Some("rtf") => Self::Rtf,
Some("svg") => Self::Svg,
Some("mp4") => Self::Mp4,
// Assume HTML when a TLD is found for eg. `wry:://tauri.studio` | `wry://hello.com`
Some(_) => Self::Html,
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
@ -118,6 +121,9 @@ mod tests {
let svg: String = MimeType::parse_from_uri("https://example.com/picture.svg").to_string();
assert_eq!(svg, String::from("image/svg+xml"));
let mp4: String = MimeType::parse_from_uri("https://example.com/video.mp4").to_string();
assert_eq!(mp4, String::from("video/mp4"));
let custom_scheme = MimeType::parse_from_uri("wry://tauri.studio").to_string();
assert_eq!(custom_scheme, String::from("text/html"));
}

View File

@ -0,0 +1,20 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
// custom wry types
mod mime_type;
mod request;
mod response;
pub use self::{
mime_type::MimeType,
request::{Request, RequestParts},
response::{Builder as ResponseBuilder, Response, ResponseParts},
};
// re-expose default http types
pub use http::{header, method, status, uri::InvalidUri, version, Uri};
// re-export httprange helper as it can be useful and we need it locally
pub use http_range::HttpRange;

View File

@ -0,0 +1,117 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::fmt;
use super::{
header::{HeaderMap, HeaderValue},
method::Method,
};
/// Represents an HTTP request from the WebView.
///
/// An HTTP request consists of a head and a potentially optional body.
///
/// ## Platform-specific
///
/// - **Linux:** Headers are not exposed.
pub struct Request {
pub head: RequestParts,
pub body: Vec<u8>,
}
/// Component parts of an HTTP `Request`
///
/// The HTTP request head consists of a method, uri, and a set of
/// header fields.
#[derive(Clone)]
pub struct RequestParts {
/// The request's method
pub method: Method,
/// The request's URI
pub uri: String,
/// The request's headers
pub headers: HeaderMap<HeaderValue>,
}
impl Request {
/// Creates a new blank `Request` with the body
#[inline]
pub fn new(body: Vec<u8>) -> Request {
Request {
head: RequestParts::new(),
body,
}
}
/// Returns a reference to the associated HTTP method.
#[inline]
pub fn method(&self) -> &Method {
&self.head.method
}
/// Returns a reference to the associated URI.
#[inline]
pub fn uri(&self) -> &str {
&self.head.uri
}
/// Returns a reference to the associated header field map.
#[inline]
pub fn headers(&self) -> &HeaderMap<HeaderValue> {
&self.head.headers
}
/// Returns a reference to the associated HTTP body.
#[inline]
pub fn body(&self) -> &Vec<u8> {
&self.body
}
/// Consumes the request returning the head and body RequestParts.
#[inline]
pub fn into_parts(self) -> (RequestParts, Vec<u8>) {
(self.head, self.body)
}
}
impl Default for Request {
fn default() -> Request {
Request::new(Vec::new())
}
}
impl fmt::Debug for Request {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Request")
.field("method", self.method())
.field("uri", &self.uri())
.field("headers", self.headers())
.field("body", self.body())
.finish()
}
}
impl RequestParts {
/// Creates a new default instance of `RequestParts`
fn new() -> RequestParts {
RequestParts {
method: Method::default(),
uri: "".into(),
headers: HeaderMap::default(),
}
}
}
impl fmt::Debug for RequestParts {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Parts")
.field("method", &self.method)
.field("uri", &self.uri)
.field("headers", &self.headers)
.finish()
}
}

View File

@ -0,0 +1,267 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use super::{
header::{HeaderMap, HeaderName, HeaderValue},
status::StatusCode,
version::Version,
};
use std::{convert::TryFrom, fmt};
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
/// Represents an HTTP response
///
/// An HTTP response consists of a head and a potentially body.
///
/// ## Platform-specific
///
/// - **Linux:** Headers and status code cannot be changed.
///
/// # Examples
///
/// ```
/// # use tauri_runtime::http::*;
///
/// let response = ResponseBuilder::new()
/// .status(202)
/// .mimetype("text/html")
/// .body("hello!".as_bytes().to_vec())
/// .unwrap();
/// ```
///
pub struct Response {
pub head: ResponseParts,
pub body: Vec<u8>,
}
/// Component parts of an HTTP `Response`
///
/// The HTTP response head consists of a status, version, and a set of
/// header fields.
#[derive(Clone)]
pub struct ResponseParts {
/// The response's status
pub status: StatusCode,
/// The response's version
pub version: Version,
/// The response's headers
pub headers: HeaderMap<HeaderValue>,
/// The response's mimetype type
pub mimetype: Option<String>,
}
/// An HTTP response builder
///
/// This type can be used to construct an instance of `Response` through a
/// builder-like pattern.
#[derive(Debug)]
pub struct Builder {
inner: Result<ResponseParts>,
}
impl Response {
/// Creates a new blank `Response` with the body
#[inline]
pub fn new(body: Vec<u8>) -> Response {
Response {
head: ResponseParts::new(),
body,
}
}
/// Returns the `StatusCode`.
#[inline]
pub fn status(&self) -> StatusCode {
self.head.status
}
/// Returns a reference to the mime type.
#[inline]
pub fn mimetype(&self) -> Option<String> {
self.head.mimetype.clone()
}
/// Returns a reference to the associated version.
#[inline]
pub fn version(&self) -> Version {
self.head.version
}
/// Returns a reference to the associated header field map.
#[inline]
pub fn headers(&self) -> &HeaderMap<HeaderValue> {
&self.head.headers
}
/// Returns a reference to the associated HTTP body.
#[inline]
pub fn body(&self) -> &Vec<u8> {
&self.body
}
}
impl Default for Response {
#[inline]
fn default() -> Response {
Response::new(Vec::new())
}
}
impl fmt::Debug for Response {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Response")
.field("status", &self.status())
.field("version", &self.version())
.field("headers", self.headers())
.field("body", self.body())
.finish()
}
}
impl ResponseParts {
/// Creates a new default instance of `ResponseParts`
fn new() -> ResponseParts {
ResponseParts {
status: StatusCode::default(),
version: Version::default(),
headers: HeaderMap::default(),
mimetype: None,
}
}
}
impl fmt::Debug for ResponseParts {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Parts")
.field("status", &self.status)
.field("version", &self.version)
.field("headers", &self.headers)
.finish()
}
}
impl Builder {
/// Creates a new default instance of `Builder` to construct either a
/// `Head` or a `Response`.
///
/// # Examples
///
/// ```
/// # use tauri_runtime::http::*;
///
/// let response = ResponseBuilder::new()
/// .status(200)
/// .mimetype("text/html")
/// .body(Vec::new())
/// .unwrap();
/// ```
#[inline]
pub fn new() -> Builder {
Builder {
inner: Ok(ResponseParts::new()),
}
}
/// Set the HTTP mimetype for this response.
pub fn mimetype(self, mimetype: &str) -> Builder {
self.and_then(move |mut head| {
head.mimetype = Some(mimetype.to_string());
Ok(head)
})
}
/// Set the HTTP status for this response.
pub fn status<T>(self, status: T) -> Builder
where
StatusCode: TryFrom<T>,
<StatusCode as TryFrom<T>>::Error: Into<crate::Error>,
{
self.and_then(move |mut head| {
head.status = TryFrom::try_from(status).map_err(Into::into)?;
Ok(head)
})
}
/// Set the HTTP version for this response.
///
/// This function will configure the HTTP version of the `Response` that
/// will be returned from `Builder::build`.
///
/// By default this is HTTP/1.1
pub fn version(self, version: Version) -> Builder {
self.and_then(move |mut head| {
head.version = version;
Ok(head)
})
}
/// Appends a header to this response builder.
///
/// This function will append the provided key/value as a header to the
/// internal `HeaderMap` being constructed. Essentially this is equivalent
/// to calling `HeaderMap::append`.
pub fn header<K, V>(self, key: K, value: V) -> Builder
where
HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<crate::Error>,
HeaderValue: TryFrom<V>,
<HeaderValue as TryFrom<V>>::Error: Into<crate::Error>,
{
self.and_then(move |mut head| {
let name = <HeaderName as TryFrom<K>>::try_from(key).map_err(Into::into)?;
let value = <HeaderValue as TryFrom<V>>::try_from(value).map_err(Into::into)?;
head.headers.append(name, value);
Ok(head)
})
}
/// "Consumes" this builder, using the provided `body` to return a
/// constructed `Response`.
///
/// # Errors
///
/// This function may return an error if any previously configured argument
/// failed to parse or get converted to the internal representation. For
/// example if an invalid `head` was specified via `header("Foo",
/// "Bar\r\n")` the error will be returned when this function is called
/// rather than when `header` was called.
///
/// # Examples
///
/// ```
/// # use tauri_runtime::http::*;
///
/// let response = ResponseBuilder::new()
/// .mimetype("text/html")
/// .body(Vec::new())
/// .unwrap();
/// ```
pub fn body(self, body: Vec<u8>) -> Result<Response> {
self.inner.map(move |head| Response { head, body })
}
// private
fn and_then<F>(self, func: F) -> Self
where
F: FnOnce(ResponseParts) -> Result<ResponseParts>,
{
Builder {
inner: self.inner.and_then(func),
}
}
}
impl Default for Builder {
#[inline]
fn default() -> Builder {
Builder {
inner: Ok(ResponseParts::new()),
}
}
}

View File

@ -13,6 +13,7 @@ use uuid::Uuid;
#[cfg(windows)]
use winapi::shared::windef::HWND;
pub mod http;
/// Create window and system tray menus.
pub mod menu;
/// Types useful for interacting with a user's monitors.
@ -27,6 +28,13 @@ use window::{
DetachedWindow, PendingWindow, WindowEvent,
};
use crate::http::{
header::{InvalidHeaderName, InvalidHeaderValue},
method::InvalidMethod,
status::InvalidStatusCode,
InvalidUri,
};
#[cfg(feature = "system-tray")]
#[non_exhaustive]
#[derive(Debug)]
@ -123,6 +131,18 @@ pub enum Error {
/// Global shortcut error.
#[error(transparent)]
GlobalShortcut(Box<dyn std::error::Error + Send>),
#[error("Invalid header name: {0}")]
InvalidHeaderName(#[from] InvalidHeaderName),
#[error("Invalid header value: {0}")]
InvalidHeaderValue(#[from] InvalidHeaderValue),
#[error("Invalid uri: {0}")]
InvalidUri(#[from] InvalidUri),
#[error("Invalid status code: {0}")]
InvalidStatusCode(#[from] InvalidStatusCode),
#[error("Invalid method: {0}")]
InvalidMethod(#[from] InvalidMethod),
#[error("Infallible error, something went really wrong: {0}")]
Infallible(#[from] std::convert::Infallible),
}
/// Result type.

View File

@ -15,17 +15,13 @@ use tauri_utils::config::{WindowConfig, WindowUrl};
#[cfg(windows)]
use winapi::shared::windef::HWND;
use std::{collections::HashMap, fmt, path::PathBuf};
type UriSchemeProtocol =
dyn Fn(&str) -> Result<Vec<u8>, Box<dyn std::error::Error>> + Send + Sync + 'static;
use std::{fmt, path::PathBuf};
/// The attributes used to create an webview.
pub struct WebviewAttributes {
pub url: WindowUrl,
pub initialization_scripts: Vec<String>,
pub data_directory: Option<PathBuf>,
pub uri_scheme_protocols: HashMap<String, Box<UriSchemeProtocol>>,
pub file_drop_handler_enabled: bool,
}
@ -47,7 +43,6 @@ impl WebviewAttributes {
url,
initialization_scripts: Vec::new(),
data_directory: None,
uri_scheme_protocols: Default::default(),
file_drop_handler_enabled: true,
}
}
@ -64,35 +59,6 @@ impl WebviewAttributes {
self
}
/// Whether the webview URI scheme protocol is defined or not.
pub fn has_uri_scheme_protocol(&self, name: &str) -> bool {
self.uri_scheme_protocols.contains_key(name)
}
/// Registers a webview protocol handler.
/// Leverages [setURLSchemeHandler](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/2875766-seturlschemehandler) on macOS,
/// [AddWebResourceRequestedFilter](https://docs.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2.addwebresourcerequestedfilter?view=webview2-dotnet-1.0.774.44) on Windows
/// and [webkit-web-context-register-uri-scheme](https://webkitgtk.org/reference/webkit2gtk/stable/WebKitWebContext.html#webkit-web-context-register-uri-scheme) on Linux.
///
/// # Arguments
///
/// * `uri_scheme` The URI scheme to register, such as `example`.
/// * `protocol` the protocol associated with the given URI scheme. It's a function that takes an URL such as `example://localhost/asset.css`.
pub fn register_uri_scheme_protocol<
N: Into<String>,
H: Fn(&str) -> Result<Vec<u8>, Box<dyn std::error::Error>> + Send + Sync + 'static,
>(
mut self,
uri_scheme: N,
protocol: H,
) -> Self {
let uri_scheme = uri_scheme.into();
self
.uri_scheme_protocols
.insert(uri_scheme, Box::new(move |data| (protocol)(data)));
self
}
/// Disables the file drop handler. This is required to use drag and drop APIs on the front end on Windows.
pub fn disable_file_drop_handler(mut self) -> Self {
self.file_drop_handler_enabled = false;
@ -203,13 +169,6 @@ pub struct RpcRequest {
pub params: Option<JsonValue>,
}
/// Uses a custom URI scheme handler to resolve file requests
pub struct CustomProtocol {
/// Handler for protocol
#[allow(clippy::type_complexity)]
pub protocol: Box<dyn Fn(&str) -> Result<Vec<u8>, Box<dyn std::error::Error>> + Send + Sync>,
}
/// The file drop event payload.
#[derive(Debug, Clone)]
#[non_exhaustive]

View File

@ -5,13 +5,20 @@
//! A layer between raw [`Runtime`] webview windows and Tauri.
use crate::{
http::{Request as HttpRequest, Response as HttpResponse},
webview::{FileDropHandler, WebviewAttributes, WebviewRpcHandler},
Dispatch, Runtime, WindowBuilder,
};
use serde::Serialize;
use tauri_utils::config::WindowConfig;
use std::hash::{Hash, Hasher};
use std::{
collections::HashMap,
hash::{Hash, Hasher},
};
type UriSchemeProtocol =
dyn Fn(&HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>> + Send + Sync + 'static;
/// UI scaling utilities.
pub mod dpi;
@ -65,6 +72,8 @@ pub struct PendingWindow<R: Runtime> {
/// The [`WebviewAttributes`] that the webview will be created with.
pub webview_attributes: WebviewAttributes,
pub uri_scheme_protocols: HashMap<String, Box<UriSchemeProtocol>>,
/// How to handle RPC calls on the webview window.
pub rpc_handler: Option<WebviewRpcHandler<R>>,
@ -85,6 +94,7 @@ impl<R: Runtime> PendingWindow<R> {
Self {
window_builder,
webview_attributes,
uri_scheme_protocols: Default::default(),
label: label.into(),
rpc_handler: None,
file_drop_handler: None,
@ -101,12 +111,27 @@ impl<R: Runtime> PendingWindow<R> {
Self {
window_builder: <<R::Dispatcher as Dispatch>::WindowBuilder>::with_config(window_config),
webview_attributes,
uri_scheme_protocols: Default::default(),
label: label.into(),
rpc_handler: None,
file_drop_handler: None,
url: "tauri://localhost".to_string(),
}
}
pub fn register_uri_scheme_protocol<
N: Into<String>,
H: Fn(&HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>> + Send + Sync + 'static,
>(
&mut self,
uri_scheme: N,
protocol: H,
) {
let uri_scheme = uri_scheme.into();
self
.uri_scheme_protocols
.insert(uri_scheme, Box::new(move |data| (protocol)(data)));
}
}
/// A webview window that is not yet managed by Tauri.

View File

@ -33,7 +33,7 @@ normal = [ "attohttpc" ]
[dependencies]
serde_json = { version = "1.0", features = [ "raw_value" ] }
serde = { version = "1.0", features = [ "derive" ] }
tokio = { version = "1.9", features = [ "rt", "rt-multi-thread", "sync", "fs" ] }
tokio = { version = "1.9", features = [ "rt", "rt-multi-thread", "sync", "fs", "io-util" ] }
futures = "0.3"
uuid = { version = "0.8", features = [ "v4" ] }
url = { version = "2.2" }
@ -164,3 +164,7 @@ path = "../../examples/state/src-tauri/src/main.rs"
[[example]]
name = "resources"
path = "../../examples/resources/src-tauri/src/main.rs"
[[example]]
name = "streaming"
path = "../../examples/streaming/src-tauri/src/main.rs"

View File

@ -8,10 +8,11 @@ pub(crate) mod tray;
use crate::{
command::{CommandArg, CommandItem},
hooks::{InvokeHandler, OnPageLoad, PageLoadPayload, SetupHook},
manager::WindowManager,
manager::{CustomProtocol, WindowManager},
plugin::{Plugin, PluginStore},
runtime::{
webview::{CustomProtocol, WebviewAttributes, WindowBuilder},
http::{Request as HttpRequest, Response as HttpResponse},
webview::{WebviewAttributes, WindowBuilder},
window::{PendingWindow, WindowEvent},
Dispatch, ExitRequestedEventAction, RunEvent, Runtime,
},
@ -562,7 +563,7 @@ pub struct Builder<R: Runtime> {
plugins: PluginStore<R>,
/// The webview protocols available to all windows.
uri_scheme_protocols: HashMap<String, Arc<CustomProtocol>>,
uri_scheme_protocols: HashMap<String, Arc<CustomProtocol<R>>>,
/// App state.
state: StateManager,
@ -803,9 +804,12 @@ impl<R: Runtime> Builder<R> {
///
/// * `uri_scheme` The URI scheme to register, such as `example`.
/// * `protocol` the protocol associated with the given URI scheme. It's a function that takes an URL such as `example://localhost/asset.css`.
pub fn register_global_uri_scheme_protocol<
pub fn register_uri_scheme_protocol<
N: Into<String>,
H: Fn(&str) -> Result<Vec<u8>, Box<dyn std::error::Error>> + Send + Sync + 'static,
H: Fn(&AppHandle<R>, &HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>>
+ Send
+ Sync
+ 'static,
>(
mut self,
uri_scheme: N,

View File

@ -65,7 +65,7 @@ use serde::Serialize;
use std::{collections::HashMap, fmt, sync::Arc};
// Export types likely to be used by the application.
pub use runtime::menu::CustomMenuItem;
pub use runtime::{http, menu::CustomMenuItem};
#[cfg(target_os = "macos")]
#[cfg_attr(doc_cfg, doc(cfg(target_os = "macos")))]

View File

@ -8,10 +8,11 @@ use crate::{
hooks::{InvokeHandler, OnPageLoad, PageLoadPayload},
plugin::PluginStore,
runtime::{
webview::{
CustomProtocol, FileDropEvent, FileDropHandler, InvokePayload, WebviewRpcHandler,
WindowBuilder,
http::{
HttpRange, MimeType, Request as HttpRequest, Response as HttpResponse,
ResponseBuilder as HttpResponseBuilder,
},
webview::{FileDropEvent, FileDropHandler, InvokePayload, WebviewRpcHandler, WindowBuilder},
window::{dpi::PhysicalSize, DetachedWindow, PendingWindow, WindowEvent},
Icon, Runtime,
},
@ -40,9 +41,11 @@ use std::{
collections::{HashMap, HashSet},
fmt,
fs::create_dir_all,
io::SeekFrom,
sync::{Arc, Mutex, MutexGuard},
};
use tauri_macros::default_runtime;
use tokio::io::{AsyncReadExt, AsyncSeekExt};
use url::Url;
const WINDOW_RESIZED_EVENT: &str = "tauri://resize";
@ -73,7 +76,7 @@ pub struct InnerWindowManager<R: Runtime> {
package_info: PackageInfo,
/// The webview protocols protocols available to all windows.
uri_scheme_protocols: HashMap<String, Arc<CustomProtocol>>,
uri_scheme_protocols: HashMap<String, Arc<CustomProtocol<R>>>,
/// The menu set to all windows.
menu: Option<Menu>,
/// Maps runtime id to a strongly typed menu id.
@ -103,6 +106,17 @@ impl<R: Runtime> fmt::Debug for InnerWindowManager<R> {
}
}
/// Uses a custom URI scheme handler to resolve file requests
pub struct CustomProtocol<R: Runtime> {
/// Handler for protocol
#[allow(clippy::type_complexity)]
pub protocol: Box<
dyn Fn(&AppHandle<R>, &HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>>
+ Send
+ Sync,
>,
}
#[default_runtime(crate::Wry, wry)]
#[derive(Debug)]
pub struct WindowManager<R: Runtime> {
@ -138,7 +152,7 @@ impl<R: Runtime> WindowManager<R> {
plugins: PluginStore<R>,
invoke_handler: Box<InvokeHandler<R>>,
on_page_load: Box<OnPageLoad<R>>,
uri_scheme_protocols: HashMap<String, Arc<CustomProtocol>>,
uri_scheme_protocols: HashMap<String, Arc<CustomProtocol<R>>>,
state: StateManager,
window_event_listeners: Vec<GlobalWindowEventListener<R>>,
(menu, menu_event_listeners): (Option<Menu>, Vec<GlobalMenuEventListener<R>>),
@ -228,6 +242,7 @@ impl<R: Runtime> WindowManager<R> {
mut pending: PendingWindow<R>,
label: &str,
pending_labels: &[String],
app_handle: AppHandle<R>,
) -> crate::Result<PendingWindow<R>> {
let is_init_global = self.inner.config.build.with_global_tauri;
let plugin_init = self
@ -257,6 +272,8 @@ impl<R: Runtime> WindowManager<R> {
));
}
pending.webview_attributes = webview_attributes;
if !pending.window_builder.has_icon() {
if let Some(default_window_icon) = &self.inner.default_window_icon {
let icon = Icon::Raw(default_window_icon.clone());
@ -270,33 +287,92 @@ impl<R: Runtime> WindowManager<R> {
}
}
for (uri_scheme, protocol) in &self.inner.uri_scheme_protocols {
if !webview_attributes.has_uri_scheme_protocol(uri_scheme) {
let protocol = protocol.clone();
webview_attributes = webview_attributes
.register_uri_scheme_protocol(uri_scheme.clone(), move |p| (protocol.protocol)(p));
}
}
let mut registered_scheme_protocols = Vec::new();
if !webview_attributes.has_uri_scheme_protocol("tauri") {
webview_attributes = webview_attributes
.register_uri_scheme_protocol("tauri", self.prepare_uri_scheme_protocol().protocol);
}
if !webview_attributes.has_uri_scheme_protocol("asset") {
webview_attributes = webview_attributes.register_uri_scheme_protocol("asset", move |url| {
#[cfg(target_os = "windows")]
let path = url.replace("asset://localhost/", "");
#[cfg(not(target_os = "windows"))]
let path = url.replace("asset://", "");
let path = percent_encoding::percent_decode(path.as_bytes())
.decode_utf8_lossy()
.to_string();
let data = crate::async_runtime::block_on(async move { tokio::fs::read(path).await })?;
Ok(data)
for (uri_scheme, protocol) in &self.inner.uri_scheme_protocols {
registered_scheme_protocols.push(uri_scheme.clone());
let protocol = protocol.clone();
let app_handle = Mutex::new(app_handle.clone());
pending.register_uri_scheme_protocol(uri_scheme.clone(), move |p| {
(protocol.protocol)(&app_handle.lock().unwrap(), p)
});
}
pending.webview_attributes = webview_attributes;
if !registered_scheme_protocols.contains(&"tauri".into()) {
pending.register_uri_scheme_protocol("tauri", self.prepare_uri_scheme_protocol());
registered_scheme_protocols.push("tauri".into());
}
if !registered_scheme_protocols.contains(&"asset".into()) {
pending.register_uri_scheme_protocol("asset", move |request| {
#[cfg(target_os = "windows")]
let path = request.uri().replace("asset://localhost/", "");
#[cfg(not(target_os = "windows"))]
let path = request.uri().replace("asset://", "");
let path = percent_encoding::percent_decode(path.as_bytes())
.decode_utf8_lossy()
.to_string();
let path_for_data = path.clone();
// handle 206 (partial range) http request
if let Some(range) = request.headers().get("range") {
let mut status_code = 200;
let path_for_data = path_for_data.clone();
let mut response = HttpResponseBuilder::new();
let (response, status_code, data) = crate::async_runtime::block_on(async move {
let mut buf = Vec::new();
let mut file = tokio::fs::File::open(path_for_data.clone()).await.unwrap();
// Get the file size
let file_size = file.metadata().await.unwrap().len();
// parse the range
let range = HttpRange::parse(range.to_str().unwrap(), file_size).unwrap();
// FIXME: Support multiple ranges
// let support only 1 range for now
let first_range = range.first();
if let Some(range) = first_range {
let mut real_length = range.length;
// prevent max_length;
// specially on webview2
if range.length > file_size / 3 {
// max size sent (400ko / request)
// as it's local file system we can afford to read more often
real_length = 1024 * 400;
}
// last byte we are reading, the length of the range include the last byte
// who should be skipped on the header
let last_byte = range.start + real_length - 1;
// partial content
status_code = 206;
response = response
.header("Connection", "Keep-Alive")
.header("Accept-Ranges", "bytes")
.header("Content-Length", real_length)
.header(
"Content-Range",
format!("bytes {}-{}/{}", range.start, last_byte, file_size),
);
file.seek(SeekFrom::Start(range.start)).await.unwrap();
file.take(real_length).read_to_end(&mut buf).await.unwrap();
}
(response, status_code, buf)
});
if !data.is_empty() {
let mime_type = MimeType::parse(&data, &path);
return response.mimetype(&mime_type).status(status_code).body(data);
}
}
let data =
crate::async_runtime::block_on(async move { tokio::fs::read(path_for_data).await })?;
let mime_type = MimeType::parse(&data, &path);
HttpResponseBuilder::new().mimetype(&mime_type).body(data)
});
}
Ok(pending)
}
@ -330,71 +406,78 @@ impl<R: Runtime> WindowManager<R> {
})
}
fn prepare_uri_scheme_protocol(&self) -> CustomProtocol {
#[allow(clippy::type_complexity)]
fn prepare_uri_scheme_protocol(
&self,
) -> Box<dyn Fn(&HttpRequest) -> Result<HttpResponse, Box<dyn std::error::Error>> + Send + Sync>
{
let assets = self.inner.assets.clone();
let manager = self.clone();
CustomProtocol {
protocol: Box::new(move |path| {
let mut path = path
.split(&['?', '#'][..])
// ignore query string
.next()
.unwrap()
.to_string()
.replace("tauri://localhost", "");
if path.ends_with('/') {
path.pop();
}
path = percent_encoding::percent_decode(path.as_bytes())
.decode_utf8_lossy()
.to_string();
let path = if path.is_empty() {
// if the url is `tauri://localhost`, we should load `index.html`
"index.html".to_string()
} else {
// skip leading `/`
path.chars().skip(1).collect::<String>()
};
let is_javascript =
path.ends_with(".js") || path.ends_with(".cjs") || path.ends_with(".mjs");
let is_html = path.ends_with(".html");
Box::new(move |request| {
let mut path = request
.uri()
.split(&['?', '#'][..])
// ignore query string
.next()
.unwrap()
.to_string()
.replace("tauri://localhost", "");
if path.ends_with('/') {
path.pop();
}
path = percent_encoding::percent_decode(path.as_bytes())
.decode_utf8_lossy()
.to_string();
let path = if path.is_empty() {
// if the url is `tauri://localhost`, we should load `index.html`
"index.html".to_string()
} else {
// skip leading `/`
path.chars().skip(1).collect::<String>()
};
let is_javascript = path.ends_with(".js") || path.ends_with(".cjs") || path.ends_with(".mjs");
let is_html = path.ends_with(".html");
let asset_response = assets
.get(&path.as_str().into())
.or_else(|| assets.get(&format!("{}/index.html", path.as_str()).into()))
.or_else(|| {
#[cfg(debug_assertions)]
eprintln!("Asset `{}` not found; fallback to index.html", path); // TODO log::error!
assets.get(&"index.html".into())
})
.ok_or(crate::Error::AssetNotFound(path))
.map(Cow::into_owned);
match asset_response {
Ok(asset) => {
if is_javascript || is_html {
let contents = String::from_utf8_lossy(&asset).into_owned();
Ok(
contents
.replacen(
"__TAURI__INVOKE_KEY_TOKEN__",
&manager.generate_invoke_key().to_string(),
1,
)
.as_bytes()
.to_vec(),
let asset_response = assets
.get(&path.as_str().into())
.or_else(|| assets.get(&format!("{}/index.html", path.as_str()).into()))
.or_else(|| {
#[cfg(debug_assertions)]
eprintln!("Asset `{}` not found; fallback to index.html", path); // TODO log::error!
assets.get(&"index.html".into())
})
.ok_or_else(|| crate::Error::AssetNotFound(path.clone()))
.map(Cow::into_owned);
match asset_response {
Ok(asset) => {
let final_data = match is_javascript || is_html {
true => String::from_utf8_lossy(&asset)
.into_owned()
.replacen(
"__TAURI__INVOKE_KEY_TOKEN__",
&manager.generate_invoke_key().to_string(),
1,
)
} else {
Ok(asset)
}
}
Err(e) => {
#[cfg(debug_assertions)]
eprintln!("{:?}", e); // TODO log::error!
Err(Box::new(e))
}
.as_bytes()
.to_vec(),
false => asset,
};
let mime_type = MimeType::parse(&final_data, &path);
Ok(
HttpResponseBuilder::new()
.mimetype(&mime_type)
.body(final_data)?,
)
}
}),
}
Err(e) => {
#[cfg(debug_assertions)]
eprintln!("{:?}", e); // TODO log::error!
Err(Box::new(e))
}
}
})
}
fn prepare_file_drop(&self, app_handle: AppHandle<R>) -> FileDropHandler<R> {
@ -560,7 +643,7 @@ impl<R: Runtime> WindowManager<R> {
if is_local {
let label = pending.label.clone();
pending = self.prepare_pending_window(pending, &label, pending_labels)?;
pending = self.prepare_pending_window(pending, &label, pending_labels, app_handle.clone())?;
pending.rpc_handler = Some(self.prepare_rpc_handler(app_handle.clone()));
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1324,6 +1324,12 @@ dependencies = [
"itoa",
]
[[package]]
name = "http-range"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eee9694f83d9b7c09682fdb32213682939507884e5bcf227be9aff5d644b90dc"
[[package]]
name = "ico"
version = "0.1.0"
@ -1457,9 +1463,9 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.52"
version = "0.3.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce791b7ca6638aae45be056e068fc756d871eb3b3b10b8efa62d1c9cec616752"
checksum = "e4bf49d50e2961077d9c99f4b7997d770a1114f087c3c2e0069b36c13fc2979d"
dependencies = [
"wasm-bindgen",
]
@ -1574,9 +1580,9 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
[[package]]
name = "memchr"
version = "2.4.0"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "memoffset"
@ -1834,9 +1840,9 @@ dependencies = [
[[package]]
name = "openssl"
version = "0.10.35"
version = "0.10.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885"
checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a"
dependencies = [
"bitflags 1.3.2",
"cfg-if 1.0.0",
@ -1854,9 +1860,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
[[package]]
name = "openssl-sys"
version = "0.9.65"
version = "0.9.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d"
checksum = "1996d2d305e561b70d1ee0c53f1542833f4e1ac6ce9a6708b6ff2738ca67dc82"
dependencies = [
"autocfg",
"cc",
@ -2898,7 +2904,7 @@ dependencies = [
[[package]]
name = "tauri"
version = "1.0.0-beta.6"
version = "1.0.0-beta.7"
dependencies = [
"attohttpc",
"base64",
@ -3011,6 +3017,9 @@ name = "tauri-runtime"
version = "0.2.0"
dependencies = [
"gtk",
"http",
"http-range",
"infer",
"serde",
"serde_json",
"tauri-utils",
@ -3160,6 +3169,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b"
dependencies = [
"autocfg",
"bytes",
"memchr",
"num_cpus",
"pin-project-lite",
]
@ -3319,9 +3330,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "wasm-bindgen"
version = "0.2.75"
version = "0.2.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b608ecc8f4198fe8680e2ed18eccab5f0cd4caaf3d83516fa5fb2e927fda2586"
checksum = "8ce9b1b516211d33767048e5d47fa2a381ed8b76fc48d2ce4aa39877f9f183e0"
dependencies = [
"cfg-if 1.0.0",
"wasm-bindgen-macro",
@ -3329,9 +3340,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.75"
version = "0.2.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "580aa3a91a63d23aac5b6b267e2d13cb4f363e31dce6c352fca4752ae12e479f"
checksum = "cfe8dc78e2326ba5f845f4b5bf548401604fa20b1dd1d365fb73b6c1d6364041"
dependencies = [
"bumpalo",
"lazy_static",
@ -3344,9 +3355,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.25"
version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16646b21c3add8e13fdb8f20172f8a28c3dbf62f45406bcff0233188226cfe0c"
checksum = "95fded345a6559c2cfee778d562300c581f7d4ff3edb9b0d230d69800d213972"
dependencies = [
"cfg-if 1.0.0",
"js-sys",
@ -3356,9 +3367,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.75"
version = "0.2.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "171ebf0ed9e1458810dfcb31f2e766ad6b3a89dbda42d8901f2b268277e5f09c"
checksum = "44468aa53335841d9d6b6c023eaab07c0cd4bddbcfdee3e2bb1e8d2cb8069fef"
dependencies = [
"quote 1.0.9",
"wasm-bindgen-macro-support",
@ -3366,9 +3377,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.75"
version = "0.2.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c2657dd393f03aa2a659c25c6ae18a13a4048cebd220e147933ea837efc589f"
checksum = "0195807922713af1e67dc66132c7328206ed9766af3858164fb583eedc25fbad"
dependencies = [
"proc-macro2",
"quote 1.0.9",
@ -3379,15 +3390,15 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.75"
version = "0.2.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e0c4a743a309662d45f4ede961d7afa4ba4131a59a639f29b0069c3798bbcc2"
checksum = "acdb075a845574a1fa5f09fd77e43f7747599301ea3417a9fbffdeedfc1f4a29"
[[package]]
name = "web-sys"
version = "0.3.52"
version = "0.3.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01c70a82d842c9979078c772d4a1344685045f1a5628f677c2b2eab4dd7d2696"
checksum = "224b2f6b67919060055ef1a67807367c2066ed520c3862cc013d26cf893a783c"
dependencies = [
"js-sys",
"wasm-bindgen",
@ -3548,8 +3559,7 @@ dependencies = [
[[package]]
name = "wry"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6864c505d06edcc8651c13e4a666596b9a1462211337d87dc9a022246d2846e8"
source = "git+https://github.com/tauri-apps/wry?branch=dev#851af5dae9f1c5a3aef021c1272b8a28119078dc"
dependencies = [
"cocoa",
"core-graphics 0.22.2",
@ -3557,6 +3567,7 @@ dependencies = [
"gio",
"glib",
"gtk",
"http",
"libc",
"log",
"objc",

View File

@ -13,10 +13,11 @@ mod menu;
#[cfg(target_os = "linux")]
use std::path::PathBuf;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use tauri::{
api::dialog::ask, async_runtime, CustomMenuItem, Event, GlobalShortcutManager, Manager,
SystemTray, SystemTrayEvent, SystemTrayMenu, WindowBuilder, WindowUrl,
api::dialog::ask, async_runtime, http::ResponseBuilder, CustomMenuItem, Event,
GlobalShortcutManager, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowBuilder,
WindowUrl,
};
#[derive(Serialize)]
@ -24,6 +25,18 @@ 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();
@ -45,6 +58,24 @@ 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());

View File

@ -75,7 +75,7 @@
}
],
"security": {
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: asset: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: asset: customprotocol: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
},
"systemTray": {
"iconPath": "../../.icons/tray_icon_with_transparency.png",

View File

@ -18,6 +18,7 @@
import Updater from "./components/Updater.svelte";
import Clipboard from "./components/Clipboard.svelte";
import WebRTC from './components/WebRTC.svelte'
import HttpForm from "./components/HttpForm.svelte";
const MENU_TOGGLE_HOTKEY = 'ctrl+b';
@ -52,6 +53,10 @@
label: "HTTP",
component: Http,
},
{
label: "HTTP Form",
component: HttpForm,
},
{
label: "Notifications",
component: Notifications,

View File

@ -0,0 +1,32 @@
<script>
let foo = 'baz'
let bar = 'qux'
let result = null
async function doPost () {
let url = navigator.userAgent.includes("Windows") ? "https://customprotocol.test/example.html" : "customprotocol://test/example.html";
const res = await fetch(url, {
method: 'POST',
body: JSON.stringify({
foo,
bar
})
})
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>

View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<video
id="video_source"
style="width: 100vw; height: 100vh"
controls=""
autoplay=""
name="media"
>
<source src="stream://example/test_video.mp4" type="video/mp4" />
</video>
<script>
(function () {
if (navigator.userAgent.includes("Windows")) {
const video = document.getElementById("video_source");
const sources = video.getElementsByTagName("source");
// on windows the custom protocl should be the host
// followed by the complete path
sources[0].src = "https://stream.example/test_video.mp4";
video.load();
}
})();
</script>
</body>
</html>

View File

@ -0,0 +1,7 @@
{
"name": "streaming",
"version": "1.0.0",
"scripts": {
"tauri": "node ../../tooling/cli.js/bin/tauri"
}
}

10
examples/streaming/src-tauri/.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
# Generated by Cargo
# will have compiled files and executables
/target/
WixTools
# These are backup files generated by rustfmt
**/*.rs.bk
config.json
bundle.json

View File

@ -0,0 +1,3 @@
// Copyright {20\d{2}(-20\d{2})?} Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

3165
examples/streaming/src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
[package]
name = "streaming"
version = "0.1.0"
description = "A very simple Tauri Appplication"
edition = "2018"
[build-dependencies]
tauri-build = { path = "../../../core/tauri-build", features = [ "codegen" ] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = [ "derive" ] }
tauri = { path = "../../../core/tauri", features = [] }
[features]
default = [ "custom-protocol" ]
custom-protocol = [ "tauri/custom-protocol" ]

View File

@ -0,0 +1,14 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use tauri_build::{try_build, Attributes, WindowsAttributes};
fn main() {
if let Err(error) = try_build(
Attributes::new()
.windows_attributes(WindowsAttributes::new().window_icon_path("../../.icons/icon.ico")),
) {
panic!("error found during tauri-build: {}", error);
}
}

View File

@ -0,0 +1,112 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
fn main() {
use std::{
io::{Read, Seek, SeekFrom},
path::PathBuf,
process::{Command, Stdio},
};
use tauri::http::{HttpRange, ResponseBuilder};
let video_file = PathBuf::from("test_video.mp4");
let video_url =
"http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_30fps_normal.mp4";
if !video_file.exists() {
// Downloading with curl this saves us from adding
// a Rust HTTP client dependency.
println!("Downloading {}", video_url);
let status = Command::new("curl")
.arg("-L")
.arg("-o")
.arg(&video_file)
.arg(video_url)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.unwrap();
assert!(status.status.success());
assert!(video_file.exists());
}
tauri::Builder::default()
.register_uri_scheme_protocol("stream", move |_app, request| {
// prepare our response
let mut response = ResponseBuilder::new();
// get the wanted path
let path = request.uri().replace("stream://", "");
if path != "example/test_video.mp4" {
// return error 404 if it's not out video
return response.mimetype("text/plain").status(404).body(Vec::new());
}
// read our file
let mut content = std::fs::File::open(&video_file)?;
let mut buf = Vec::new();
// default status code
let mut status_code = 200;
// if the webview sent a range header, we need to send a 206 in return
// Actually only macOS and Windows are supported. Linux will ALWAYS return empty headers.
if let Some(range) = request.headers().get("range") {
// Get the file size
let file_size = content.metadata().unwrap().len();
// we parse the range header with tauri helper
let range = HttpRange::parse(range.to_str().unwrap(), file_size).unwrap();
// let support only 1 range for now
let first_range = range.first();
if let Some(range) = first_range {
let mut real_length = range.length;
// prevent max_length;
// specially on webview2
if range.length > file_size / 3 {
// max size sent (400ko / request)
// as it's local file system we can afford to read more often
real_length = 1024 * 400;
}
// last byte we are reading, the length of the range include the last byte
// who should be skipped on the header
let last_byte = range.start + real_length - 1;
// partial content
status_code = 206;
// Only macOS and Windows are supported, if you set headers in linux they are ignored
response = response
.header("Connection", "Keep-Alive")
.header("Accept-Ranges", "bytes")
.header("Content-Length", real_length)
.header(
"Content-Range",
format!("bytes {}-{}/{}", range.start, last_byte, file_size),
);
// FIXME: Add ETag support (caching on the webview)
// seek our file bytes
content.seek(SeekFrom::Start(range.start))?;
content.take(real_length).read_to_end(&mut buf)?;
} else {
content.read_to_end(&mut buf)?;
}
}
response.mimetype("video/mp4").status(status_code).body(buf)
})
.run(tauri::generate_context!(
"../../examples/streaming/src-tauri/tauri.conf.json"
))
.expect("error while running tauri application");
}

View File

@ -0,0 +1,56 @@
{
"build": {
"distDir": ["../index.html"],
"devPath": ["../index.html"],
"beforeDevCommand": "",
"beforeBuildCommand": ""
},
"tauri": {
"bundle": {
"active": true,
"targets": "all",
"identifier": "com.tauri.dev",
"icon": [
"../../.icons/32x32.png",
"../../.icons/128x128.png",
"../../.icons/128x128@2x.png",
"../../.icons/icon.icns",
"../../.icons/icon.ico"
],
"resources": [],
"externalBin": [],
"copyright": "",
"category": "DeveloperTool",
"shortDescription": "",
"longDescription": "",
"deb": {
"depends": [],
"useBootstrapper": false
},
"macOS": {
"frameworks": [],
"minimumSystemVersion": "",
"useBootstrapper": false,
"exceptionDomain": ""
}
},
"allowlist": {
"all": false
},
"windows": [
{
"title": "Welcome to Tauri!",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: stream: 'unsafe-eval' 'unsafe-inline' 'self'"
},
"updater": {
"active": false
}
}
}