mirror of
https://github.com/tauri-apps/tauri.git
synced 2024-12-01 11:13:40 +03:00
refactor: custom protocol (#2503)
Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
This commit is contained in:
parent
994b5325dd
commit
539e4489e0
6
.changes/refactor-register-uri-scheme-protocol.md
Normal file
6
.changes/refactor-register-uri-scheme-protocol.md
Normal 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.
|
7
.changes/tauri-protocol.md
Normal file
7
.changes/tauri-protocol.md
Normal 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
1
.gitignore
vendored
@ -93,3 +93,4 @@ __handlers__/
|
||||
|
||||
# benches
|
||||
gh-pages
|
||||
test_video.mp4
|
@ -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()
|
||||
|
@ -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"
|
||||
|
@ -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"));
|
||||
}
|
20
core/tauri-runtime/src/http/mod.rs
Normal file
20
core/tauri-runtime/src/http/mod.rs
Normal 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;
|
117
core/tauri-runtime/src/http/request.rs
Normal file
117
core/tauri-runtime/src/http/request.rs
Normal 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()
|
||||
}
|
||||
}
|
267
core/tauri-runtime/src/http/response.rs
Normal file
267
core/tauri-runtime/src/http/response.rs
Normal 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()),
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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]
|
||||
|
@ -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.
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
|
@ -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")))]
|
||||
|
@ -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
61
examples/api/src-tauri/Cargo.lock
generated
61
examples/api/src-tauri/Cargo.lock
generated
@ -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",
|
||||
|
@ -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());
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
32
examples/api/src/components/HttpForm.svelte
Normal file
32
examples/api/src/components/HttpForm.svelte
Normal 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>
|
31
examples/streaming/index.html
Normal file
31
examples/streaming/index.html
Normal 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>
|
7
examples/streaming/package.json
Normal file
7
examples/streaming/package.json
Normal 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
10
examples/streaming/src-tauri/.gitignore
vendored
Normal 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
|
3
examples/streaming/src-tauri/.license_template
Normal file
3
examples/streaming/src-tauri/.license_template
Normal 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
3165
examples/streaming/src-tauri/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
examples/streaming/src-tauri/Cargo.toml
Normal file
17
examples/streaming/src-tauri/Cargo.toml
Normal 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" ]
|
14
examples/streaming/src-tauri/build.rs
Normal file
14
examples/streaming/src-tauri/build.rs
Normal 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);
|
||||
}
|
||||
}
|
112
examples/streaming/src-tauri/src/main.rs
Normal file
112
examples/streaming/src-tauri/src/main.rs
Normal 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");
|
||||
}
|
56
examples/streaming/src-tauri/tauri.conf.json
Normal file
56
examples/streaming/src-tauri/tauri.conf.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user