diff --git a/.changes/http-api-stream.md b/.changes/http-api-stream.md new file mode 100644 index 00000000000..ca2b7c6fb89 --- /dev/null +++ b/.changes/http-api-stream.md @@ -0,0 +1,5 @@ +--- +"tauri": patch +--- + +Added `bytes_stream` method to `tauri::api::http::Response`. diff --git a/.changes/updater-download-events.md b/.changes/updater-download-events.md new file mode 100644 index 00000000000..5a56f4d8e99 --- /dev/null +++ b/.changes/updater-download-events.md @@ -0,0 +1,5 @@ +--- +"tauri": patch +--- + +Added download progress events to the updater. diff --git a/core/tauri/Cargo.toml b/core/tauri/Cargo.toml index 9798c783f73..b9e05ce4ae0 100644 --- a/core/tauri/Cargo.toml +++ b/core/tauri/Cargo.toml @@ -73,7 +73,7 @@ percent-encoding = "2.1" base64 = { version = "0.13", optional = true } clap = { version = "3", optional = true } notify-rust = { version = "4.5", optional = true } -reqwest = { version = "0.11", features = [ "json", "multipart" ], optional = true } +reqwest = { version = "0.11", features = [ "json", "multipart", "stream" ], optional = true } bytes = { version = "1", features = [ "serde" ], optional = true } attohttpc = { version = "0.18", features = [ "json", "form" ], optional = true } open = { version = "2.0", optional = true } @@ -137,10 +137,10 @@ updater = [ "fs-extract-api" ] __updater-docs = [ "minisign-verify", "base64", "http-api", "dialog-ask" ] -http-api = [ "attohttpc" ] +http-api = [ "attohttpc", "bytes" ] shell-open-api = [ "open", "regex", "tauri-macros/shell-scope" ] fs-extract-api = [ "zip" ] -reqwest-client = [ "reqwest", "bytes" ] +reqwest-client = [ "reqwest" ] process-command-api = [ "shared_child", "os_pipe", "memchr" ] dialog = [ "rfd" ] notification = [ "notify-rust" ] diff --git a/core/tauri/src/api/http.rs b/core/tauri/src/api/http.rs index 7b5bce2bf19..2f2e955acc2 100644 --- a/core/tauri/src/api/http.rs +++ b/core/tauri/src/api/http.rs @@ -5,6 +5,7 @@ //! Types and functions related to HTTP request. use http::{header::HeaderName, Method}; +pub use http::{HeaderMap, StatusCode}; use serde::{Deserialize, Serialize}; use serde_json::Value; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -352,10 +353,46 @@ pub struct Response(ResponseType, reqwest::Response); #[derive(Debug)] pub struct Response(ResponseType, attohttpc::Response, Url); +#[cfg(not(feature = "reqwest-client"))] +struct AttohttpcByteReader(attohttpc::ResponseReader); + +#[cfg(not(feature = "reqwest-client"))] +impl futures::Stream for AttohttpcByteReader { + type Item = crate::api::Result; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + _cx: &mut futures::task::Context<'_>, + ) -> futures::task::Poll> { + use std::io::Read; + let mut buf = [0; 256]; + match self.0.read(&mut buf) { + Ok(b) => { + if b == 0 { + futures::task::Poll::Ready(None) + } else { + futures::task::Poll::Ready(Some(Ok(buf[0..b].to_vec().into()))) + } + } + Err(_) => futures::task::Poll::Ready(None), + } + } +} + impl Response { + /// Get the [`StatusCode`] of this Response. + pub fn status(&self) -> StatusCode { + self.1.status() + } + + /// Get the headers of this Response. + pub fn headers(&self) -> &HeaderMap { + self.1.headers() + } + /// Reads the response as raw bytes. pub async fn bytes(self) -> crate::api::Result { - let status = self.1.status().as_u16(); + let status = self.status().as_u16(); #[cfg(feature = "reqwest-client")] let data = self.1.bytes().await?.to_vec(); #[cfg(not(feature = "reqwest-client"))] @@ -363,6 +400,38 @@ impl Response { Ok(RawResponse { status, data }) } + /// Convert the response into a Stream of [`bytes::Bytes`] from the body. + /// + /// # Examples + /// + /// ```no_run + /// use futures::StreamExt; + /// + /// # async fn run() -> Result<(), Box> { + /// let client = tauri::api::http::ClientBuilder::new().build()?; + /// let mut stream = client.send(tauri::api::http::HttpRequestBuilder::new("GET", "http://httpbin.org/ip")?) + /// .await? + /// .bytes_stream(); + /// + /// while let Some(item) = stream.next().await { + /// println!("Chunk: {:?}", item?); + /// } + /// # Ok(()) + /// # } + /// ``` + pub fn bytes_stream(self) -> impl futures::Stream> { + #[cfg(not(feature = "reqwest-client"))] + { + let (_, _, reader) = self.1.split(); + AttohttpcByteReader(reader) + } + #[cfg(feature = "reqwest-client")] + { + use futures::StreamExt; + self.1.bytes_stream().map(|res| res.map_err(Into::into)) + } + } + /// Reads the response. /// /// Note that the body is serialized to a [`Value`]. diff --git a/core/tauri/src/app.rs b/core/tauri/src/app.rs index 7dd365ac3f0..f8c7c496ef2 100644 --- a/core/tauri/src/app.rs +++ b/core/tauri/src/app.rs @@ -579,13 +579,9 @@ impl App { impl App { /// Runs the updater hook with built-in dialog. fn run_updater_dialog(&self) { - let updater_config = self.manager.config().tauri.updater.clone(); - let package_info = self.manager.package_info().clone(); let handle = self.handle(); - crate::async_runtime::spawn(async move { - updater::check_update_with_dialog(updater_config, package_info, handle).await - }); + crate::async_runtime::spawn(async move { updater::check_update_with_dialog(handle).await }); } fn run_updater(&self) { @@ -597,22 +593,18 @@ impl App { if updater_config.dialog { // if updater dialog is enabled spawn a new task self.run_updater_dialog(); - let config = self.manager.config().tauri.updater.clone(); - let package_info = self.manager.package_info().clone(); // When dialog is enabled, if user want to recheck // if an update is available after first start // invoke the Event `tauri://update` from JS or rust side. handle.listen_global(updater::EVENT_CHECK_UPDATE, move |_msg| { let handle = handle_.clone(); - let package_info = package_info.clone(); - let config = config.clone(); // re-spawn task inside tokyo to launch the download // we don't need to emit anything as everything is handled // by the process (user is asked to restart at the end) // and it's handled by the updater - crate::async_runtime::spawn(async move { - updater::check_update_with_dialog(config, package_info, handle).await - }); + crate::async_runtime::spawn( + async move { updater::check_update_with_dialog(handle).await }, + ); }); } else { // we only listen for `tauri://update` diff --git a/core/tauri/src/lib.rs b/core/tauri/src/lib.rs index d2a5020b43b..78ea60387c3 100644 --- a/core/tauri/src/lib.rs +++ b/core/tauri/src/lib.rs @@ -242,8 +242,16 @@ pub enum UpdaterEvent { /// The update version. version: String, }, - /// The update is pending. + /// The update is pending and about to be downloaded. Pending, + /// The update download received a progress event. + DownloadProgress { + /// The amount that was downloaded on this iteration. + /// Does not accumulate with previous chunks. + chunk_length: usize, + /// The total + content_length: Option, + }, /// The update has been applied and the app is now up to date. Updated, /// The app is already up to date. diff --git a/core/tauri/src/updater/core.rs b/core/tauri/src/updater/core.rs index efae7f3627f..c3201850c1c 100644 --- a/core/tauri/src/updater/core.rs +++ b/core/tauri/src/updater/core.rs @@ -5,11 +5,15 @@ use super::error::{Error, Result}; #[cfg(feature = "updater")] use crate::api::file::{ArchiveFormat, Extract, Move}; -use crate::api::{ - http::{ClientBuilder, HttpRequestBuilder}, - version, +use crate::{ + api::{ + http::{ClientBuilder, HttpRequestBuilder}, + version, + }, + AppHandle, Manager, Runtime, }; use base64::decode; +use futures::StreamExt; use http::StatusCode; use minisign_verify::{PublicKey, Signature}; use tauri_utils::{platform::current_exe, Env}; @@ -176,9 +180,9 @@ impl RemoteRelease { } #[derive(Debug)] -pub struct UpdateBuilder<'a> { - /// Environment information. - pub env: Env, +pub struct UpdateBuilder<'a, R: Runtime> { + /// Application handle. + pub app: AppHandle, /// Current version we are running to compare with announced version pub current_version: &'a str, /// The URLs to checks updates. We suggest at least one fallback on a different domain. @@ -190,10 +194,10 @@ pub struct UpdateBuilder<'a> { } // Create new updater instance and return an Update -impl<'a> UpdateBuilder<'a> { - pub fn new(env: Env) -> Self { +impl<'a, R: Runtime> UpdateBuilder<'a, R> { + pub fn new(app: AppHandle) -> Self { UpdateBuilder { - env, + app, urls: Vec::new(), target: None, executable_path: None, @@ -247,7 +251,7 @@ impl<'a> UpdateBuilder<'a> { self } - pub async fn build(self) -> Result { + pub async fn build(self) -> Result> { let mut remote_release: Option = None; // make sure we have at least one url @@ -271,7 +275,7 @@ impl<'a> UpdateBuilder<'a> { .ok_or(Error::UnsupportedPlatform)?; // Get the extract_path from the provided executable_path - let extract_path = extract_path_from_executable(&self.env, &executable_path); + let extract_path = extract_path_from_executable(&self.app.state::(), &executable_path); // Set SSL certs for linux if they aren't available. // We do not require to recheck in the download_and_install as we use @@ -364,7 +368,7 @@ impl<'a> UpdateBuilder<'a> { // create our new updater Ok(Update { - env: self.env, + app: self.app, target, extract_path, should_update, @@ -380,14 +384,14 @@ impl<'a> UpdateBuilder<'a> { } } -pub fn builder<'a>(env: Env) -> UpdateBuilder<'a> { - UpdateBuilder::new(env) +pub fn builder<'a, R: Runtime>(app: AppHandle) -> UpdateBuilder<'a, R> { + UpdateBuilder::new(app) } -#[derive(Debug, Clone)] -pub struct Update { - /// Environment information. - pub env: Env, +#[derive(Debug)] +pub struct Update { + /// Application handle. + pub app: AppHandle, /// Update description pub body: Option, /// Should we update or not @@ -413,17 +417,40 @@ pub struct Update { with_elevated_task: bool, } -impl Update { +impl Clone for Update { + fn clone(&self) -> Self { + Self { + app: self.app.clone(), + body: self.body.clone(), + should_update: self.should_update, + version: self.version.clone(), + current_version: self.current_version.clone(), + date: self.date.clone(), + target: self.target.clone(), + extract_path: self.extract_path.clone(), + download_url: self.download_url.clone(), + signature: self.signature.clone(), + #[cfg(target_os = "windows")] + with_elevated_task: self.with_elevated_task, + } + } +} + +impl Update { // Download and install our update // @todo(lemarier): Split into download and install (two step) but need to be thread safe - pub async fn download_and_install(&self, pub_key: String) -> Result { + pub(crate) async fn download_and_install)>( + &self, + pub_key: String, + on_chunk: F, + ) -> Result { // make sure we can install the update on linux // We fail here because later we can add more linux support // actually if we use APPIMAGE, our extract path should already // be set with our APPIMAGE env variable, we don't need to do // anythin with it yet #[cfg(target_os = "linux")] - if self.env.appimage.is_none() { + if self.app.state::().appimage.is_none() { return Err(Error::UnsupportedPlatform); } @@ -433,7 +460,7 @@ impl Update { headers.insert("User-Agent".into(), "tauri/updater".into()); // Create our request - let resp = ClientBuilder::new() + let response = ClientBuilder::new() .build()? .send( HttpRequestBuilder::new("GET", self.download_url.as_str())? @@ -441,23 +468,33 @@ impl Update { // wait 20sec for the firewall .timeout(20), ) - .await? - .bytes() .await?; // make sure it's success - if !StatusCode::from_u16(resp.status) - .map_err(|e| Error::Network(e.to_string()))? - .is_success() - { + if !response.status().is_success() { return Err(Error::Network(format!( "Download request failed with status: {}", - resp.status + response.status() ))); } + let content_length: Option = response + .headers() + .get("Content-Length") + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.parse().ok()); + + let mut buffer = Vec::new(); + let mut stream = response.bytes_stream(); + while let Some(chunk) = stream.next().await { + let chunk = chunk?; + let bytes = chunk.as_ref().to_vec(); + on_chunk(bytes.len(), content_length); + buffer.extend(bytes); + } + // create memory buffer from our archive (Seek + Read) - let mut archive_buffer = Cursor::new(resp.data); + let mut archive_buffer = Cursor::new(buffer); // We need an announced signature by the server // if there is no signature, bail out. @@ -875,7 +912,8 @@ mod test { .with_body(generate_sample_raw_json()) .create(); - let check_update = block!(builder(Default::default()) + let app = crate::test::mock_app(); + let check_update = block!(builder(app.handle()) .current_version("0.0.0") .url(mockito::server_url()) .build()); @@ -894,7 +932,8 @@ mod test { .with_body(generate_sample_raw_json()) .create(); - let check_update = block!(builder(Default::default()) + let app = crate::test::mock_app(); + let check_update = block!(builder(app.handle()) .current_version("0.0.0") .url(mockito::server_url()) .build()); @@ -913,7 +952,8 @@ mod test { .with_body(generate_sample_raw_json()) .create(); - let check_update = block!(builder(Default::default()) + let app = crate::test::mock_app(); + let check_update = block!(builder(app.handle()) .current_version("0.0.0") .target("win64") .url(mockito::server_url()) @@ -939,7 +979,8 @@ mod test { .with_body(generate_sample_raw_json()) .create(); - let check_update = block!(builder(Default::default()) + let app = crate::test::mock_app(); + let check_update = block!(builder(app.handle()) .current_version("10.0.0") .url(mockito::server_url()) .build()); @@ -962,7 +1003,8 @@ mod test { )) .create(); - let check_update = block!(builder(Default::default()) + let app = crate::test::mock_app(); + let check_update = block!(builder(app.handle()) .current_version("1.0.0") .url(format!( "{}/darwin/{{{{current_version}}}}", @@ -988,7 +1030,8 @@ mod test { )) .create(); - let check_update = block!(builder(Default::default()) + let app = crate::test::mock_app(); + let check_update = block!(builder(app.handle()) .current_version("1.0.0") .url( url::Url::parse(&format!( @@ -1005,7 +1048,8 @@ mod test { assert!(updater.should_update); - let check_update = block!(builder(Default::default()) + let app = crate::test::mock_app(); + let check_update = block!(builder(app.handle()) .current_version("1.0.0") .urls(&[url::Url::parse(&format!( "{}/darwin/{{{{current_version}}}}", @@ -1034,7 +1078,8 @@ mod test { )) .create(); - let check_update = block!(builder(Default::default()) + let app = crate::test::mock_app(); + let check_update = block!(builder(app.handle()) .current_version("1.0.0") .url(format!( "{}/win64/{{{{current_version}}}}", @@ -1060,7 +1105,8 @@ mod test { )) .create(); - let check_update = block!(builder(Default::default()) + let app = crate::test::mock_app(); + let check_update = block!(builder(app.handle()) .current_version("10.0.0") .url(format!( "{}/darwin/{{{{current_version}}}}", @@ -1082,7 +1128,8 @@ mod test { .with_body(generate_sample_raw_json()) .create(); - let check_update = block!(builder(Default::default()) + let app = crate::test::mock_app(); + let check_update = block!(builder(app.handle()) .url("http://badurl.www.tld/1".into()) .url(mockito::server_url()) .current_version("0.0.1") @@ -1102,7 +1149,8 @@ mod test { .with_body(generate_sample_raw_json()) .create(); - let check_update = block!(builder(Default::default()) + let app = crate::test::mock_app(); + let check_update = block!(builder(app.handle()) .urls(&["http://badurl.www.tld/1".into(), mockito::server_url(),]) .current_version("0.0.1") .build()); @@ -1121,7 +1169,8 @@ mod test { .with_body(generate_sample_bad_json()) .create(); - let check_update = block!(builder(Default::default()) + let app = crate::test::mock_app(); + let check_update = block!(builder(app.handle()) .url(mockito::server_url()) .current_version("0.0.1") .build()); @@ -1202,7 +1251,8 @@ mod test { let my_executable = &tmp_dir_path.join("my_app.exe"); // configure the updater - let check_update = block!(builder(Default::default()) + let app = crate::test::mock_app(); + let check_update = block!(builder(app.handle()) .url(mockito::server_url()) // It should represent the executable path, that's why we add my_app.exe in our // test path -- in production you shouldn't have to provide it @@ -1228,7 +1278,7 @@ mod test { assert_eq!(updater.version, "2.0.1"); // download, install and validate signature - let install_process = block!(updater.download_and_install(pubkey)); + let install_process = block!(updater.download_and_install(pubkey, |_, _| ())); assert!(install_process.is_ok()); // make sure the extraction went well (it should have skipped the main app.app folder) diff --git a/core/tauri/src/updater/mod.rs b/core/tauri/src/updater/mod.rs index 64244f17c56..b0a493e7aad 100644 --- a/core/tauri/src/updater/mod.rs +++ b/core/tauri/src/updater/mod.rs @@ -73,7 +73,7 @@ //! import { checkUpdate, installUpdate } from "@tauri-apps/api/updater"; //! //! try { -//! const {shouldUpdate, manifest} = await checkUpdate(); +//! const { shouldUpdate, manifest } = await checkUpdate(); //! //! if (shouldUpdate) { //! // display dialog @@ -93,21 +93,28 @@ //! //! ### Initialize updater and check if a new version is available //! -//! #### If a new version is available, the event `tauri://update-available` is emitted. -//! //! Event : `tauri://update` //! -//! ### Rust -//! ```ignore -//! dispatcher.emit("tauri://update", None); +//! #### Rust +//! ```no_run +//! tauri::Builder::default() +//! .setup(|app| { +//! let handle = app.handle(); +//! tauri::async_runtime::spawn(async move { +//! let response = handle.check_for_updates().await; +//! }); +//! Ok(()) +//! }); //! ``` //! -//! ### Javascript +//! #### Javascript //! ```js //! import { emit } from "@tauri-apps/api/event"; //! emit("tauri://update"); //! ``` //! +//! **If a new version is available, the event `tauri://update-available` is emitted.** +//! //! ### Listen New Update Available //! //! Event : `tauri://update-available` @@ -119,14 +126,26 @@ //! body Note announced by the server //! ``` //! -//! ### Rust -//! ```ignore -//! dispatcher.listen("tauri://update-available", move |msg| { -//! println("New version available: {:?}", msg); -//! }) +//! #### Rust +//! ```no_run +//! let app = tauri::Builder::default() +//! // on an actual app, remove the string argument +//! .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) +//! .expect("error while building tauri application"); +//! app.run(|_app_handle, event| match event { +//! tauri::RunEvent::Updater(updater_event) => { +//! match updater_event { +//! tauri::UpdaterEvent::UpdateAvailable { body, date, version } => { +//! println!("update available {} {} {}", body, date, version); +//! } +//! _ => (), +//! } +//! } +//! _ => {} +//! }); //! ``` //! -//! ### Javascript +//! #### Javascript //! ```js //! import { listen } from "@tauri-apps/api/event"; //! listen("tauri://update-available", function (res) { @@ -140,42 +159,124 @@ //! //! Event : `tauri://update-install` //! -//! ### Rust -//! ```ignore -//! dispatcher.emit("tauri://update-install", None); +//! #### Rust +//! ```no_run +//! tauri::Builder::default() +//! .setup(|app| { +//! let handle = app.handle(); +//! tauri::async_runtime::spawn(async move { +//! match handle.check_for_updates().await { +//! Ok(update) => { +//! if update.is_update_available() { +//! update.download_and_install().await.unwrap(); +//! } +//! } +//! Err(e) => { +//! println!("failed to update: {}", e); +//! } +//! } +//! }); +//! Ok(()) +//! }); //! ``` //! -//! ### Javascript +//! #### Javascript //! ```js //! import { emit } from "@tauri-apps/api/event"; //! emit("tauri://update-install"); //! ``` //! -//! ### Listen Install Progress +//! ### Listen Download Progress +//! +//! The event payload informs the length of the chunk that was just downloaded, and the total download size if known. +//! +//! #### Rust +//! ```no_run +//! let app = tauri::Builder::default() +//! // on an actual app, remove the string argument +//! .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) +//! .expect("error while building tauri application"); +//! app.run(|_app_handle, event| match event { +//! tauri::RunEvent::Updater(updater_event) => { +//! match updater_event { +//! tauri::UpdaterEvent::DownloadProgress { chunk_length, content_length } => { +//! println!("downloaded {} of {:?}", chunk_length, content_length); +//! } +//! _ => (), +//! } +//! } +//! _ => {} +//! }); +//! ``` //! -//! Event : `tauri://update-status` +//! #### Javascript +//! +//! Event : `tauri://update-download-progress` //! //! Emitted data: //! ```text -//! status [ERROR/PENDING/DONE] -//! error String/null +//! chunkLength number +//! contentLength number/null //! ``` //! -//! PENDING is emitted when the download is started and DONE when the install is complete. You can then ask to restart the application. +//! ```js +//! import { listen } from "@tauri-apps/api/event"; +//! listen<{ chunkLength: number, contentLength?: number }>("tauri://update-download-progress", function (event) { +//! console.log(`downloaded ${event.payload.chunkLength} of ${event.payload.contentLength}`); +//! }); +//! ``` //! -//! ERROR is emitted when there is an error with the updater. We suggest to listen to this event even if the dialog is enabled. +//! ### Listen Install Progress //! -//! ### Rust -//! ```ignore -//! dispatcher.listen("tauri://update-status", move |msg| { -//! println("New status: {:?}", msg); -//! }) +//! **Pending** is emitted when the download is started and **Done** when the install is complete. You can then ask to restart the application. +//! +//! **UpToDate** is emitted when the app already has the latest version installed and an update is not needed. +//! +//! **Error** is emitted when there is an error with the updater. We suggest to listen to this event even if the dialog is enabled. +//! +//! #### Rust +//! ```no_run +//! let app = tauri::Builder::default() +//! // on an actual app, remove the string argument +//! .build(tauri::generate_context!("test/fixture/src-tauri/tauri.conf.json")) +//! .expect("error while building tauri application"); +//! app.run(|_app_handle, event| match event { +//! tauri::RunEvent::Updater(updater_event) => { +//! match updater_event { +//! tauri::UpdaterEvent::UpdateAvailable { body, date, version } => { +//! println!("update available {} {} {}", body, date, version); +//! } +//! tauri::UpdaterEvent::Pending => { +//! println!("update is pending!"); +//! } +//! tauri::UpdaterEvent::Updated => { +//! println!("app has been updated"); +//! } +//! tauri::UpdaterEvent::AlreadyUpToDate => { +//! println!("app is already up to date"); +//! } +//! tauri::UpdaterEvent::Error(error) => { +//! println!("failed to update: {}", error); +//! } +//! _ => (), +//! } +//! } +//! _ => {} +//! }); +//! ``` +//! +//! #### Javascript +//! Event : `tauri://update-status` +//! +//! Emitted data: +//! ```text +//! status ERROR | PENDING | UPTODATE | DONE +//! error string/null //! ``` //! -//! ### Javascript //! ```js //! import { listen } from "@tauri-apps/api/event"; -//! listen("tauri://update-status", function (res) { +//! listen<{ status: string, error?: string }>("tauri://update-status", function (res) { //! console.log("New status: ", res); //! }); //! ``` @@ -335,8 +436,8 @@ pub use self::error::Error; pub type Result = std::result::Result; use crate::{ - api::dialog::blocking::ask, runtime::EventLoopProxy, utils::config::UpdaterConfig, AppHandle, - Env, EventLoopMessage, Manager, Runtime, UpdaterEvent, + api::dialog::blocking::ask, runtime::EventLoopProxy, AppHandle, EventLoopMessage, Manager, + Runtime, UpdaterEvent, }; /// Check for new updates @@ -349,6 +450,8 @@ pub const EVENT_INSTALL_UPDATE: &str = "tauri://update-install"; /// always listen for this event. It'll send you the install progress /// and any error triggered during update check and install pub const EVENT_STATUS_UPDATE: &str = "tauri://update-status"; +/// The name of the event that is emitted on download progress. +pub const EVENT_DOWNLOAD_PROGRESS: &str = "tauri://update-download-progress"; /// this is the status emitted when the download start pub const EVENT_STATUS_PENDING: &str = "PENDING"; /// When you got this status, something went wrong @@ -365,6 +468,13 @@ struct StatusEvent { error: Option, } +#[derive(Clone, serde::Serialize)] +#[serde(rename_all = "camelCase")] +struct DownloadProgressEvent { + chunk_length: usize, + content_length: Option, +} + #[derive(Clone, serde::Serialize)] struct UpdateManifest { version: String, @@ -372,17 +482,15 @@ struct UpdateManifest { body: String, } -/// The response of an updater [`check`]. +/// The response of an updater check. pub struct UpdateResponse { - update: core::Update, - handle: AppHandle, + update: core::Update, } impl Clone for UpdateResponse { fn clone(&self) -> Self { Self { update: self.update.clone(), - handle: self.handle.clone(), } } } @@ -405,24 +513,21 @@ impl UpdateResponse { /// Downloads and installs the update. pub async fn download_and_install(self) -> Result<()> { - download_and_install(self.handle, self.update).await + download_and_install(self.update).await } } /// Check if there is any new update with builtin dialog. -pub(crate) async fn check_update_with_dialog( - updater_config: UpdaterConfig, - package_info: crate::PackageInfo, - handle: AppHandle, -) { +pub(crate) async fn check_update_with_dialog(handle: AppHandle) { + let updater_config = handle.config().tauri.updater.clone(); + let package_info = handle.package_info().clone(); if let Some(endpoints) = updater_config.endpoints.clone() { let endpoints = endpoints .iter() .map(|e| e.to_string()) .collect::>(); - let env = handle.state::().inner().clone(); // check updates - match self::core::builder(env) + match self::core::builder(handle.clone()) .urls(&endpoints[..]) .current_version(&package_info.version) .build() @@ -434,15 +539,8 @@ pub(crate) async fn check_update_with_dialog( // if dialog enabled only if updater.should_update && updater_config.dialog { let body = updater.body.clone().unwrap_or_else(|| String::from("")); - let handle_ = handle.clone(); - let dialog = prompt_for_install( - handle_, - &updater.clone(), - &package_info.name, - &body.clone(), - pubkey, - ) - .await; + let dialog = + prompt_for_install(&updater.clone(), &package_info.name, &body.clone(), pubkey).await; if let Err(e) = dialog { send_status_update(&handle, UpdaterEvent::Error(e.to_string())); @@ -469,31 +567,32 @@ pub(crate) fn listener(handle: AppHandle) { }); } -pub(crate) async fn download_and_install( - handle: AppHandle, - update: core::Update, -) -> Result<()> { - let update = update.clone(); - +pub(crate) async fn download_and_install(update: core::Update) -> Result<()> { // Start installation // emit {"status": "PENDING"} - send_status_update(&handle, UpdaterEvent::Pending); + send_status_update(&update.app, UpdaterEvent::Pending); + + let handle = update.app.clone(); // Launch updater download process // macOS we display the `Ready to restart dialog` asking to restart // Windows is closing the current App and launch the downloaded MSI when ready (the process stop here) // Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here) let update_result = update - .clone() - .download_and_install(handle.config().tauri.updater.pubkey.clone()) + .download_and_install( + update.app.config().tauri.updater.pubkey.clone(), + move |chunk_length, content_length| { + send_download_progress_event(&handle, chunk_length, content_length); + }, + ) .await; if let Err(err) = &update_result { // emit {"status": "ERROR", "error": "The error message"} - send_status_update(&handle, UpdaterEvent::Error(err.to_string())); + send_status_update(&update.app, UpdaterEvent::Error(err.to_string())); } else { // emit {"status": "DONE"} - send_status_update(&handle, UpdaterEvent::Updated); + send_status_update(&update.app, UpdaterEvent::Updated); } update_result } @@ -512,9 +611,7 @@ pub(crate) async fn check(handle: AppHandle) -> Result>(); // check updates - let env = handle.state::().inner().clone(); - - match self::core::builder(env) + match self::core::builder(handle.clone()) .urls(&endpoints[..]) .current_version(&package_info.version) .build() @@ -543,17 +640,16 @@ pub(crate) async fn check(handle: AppHandle) -> Result { send_status_update(&handle, UpdaterEvent::Error(e.to_string())); @@ -562,6 +658,28 @@ pub(crate) async fn check(handle: AppHandle) -> Result( + handle: &AppHandle, + chunk_length: usize, + content_length: Option, +) { + let _ = handle.emit_all( + EVENT_DOWNLOAD_PROGRESS, + DownloadProgressEvent { + chunk_length, + content_length, + }, + ); + let _ = + handle + .create_proxy() + .send_event(EventLoopMessage::Updater(UpdaterEvent::DownloadProgress { + chunk_length, + content_length, + })); +} + // Send a status update via `tauri://update-status` event. fn send_status_update(handle: &AppHandle, message: UpdaterEvent) { let _ = handle.emit_all( @@ -586,15 +704,14 @@ fn send_status_update(handle: &AppHandle, message: UpdaterEvent) // Prompt a dialog asking if the user want to install the new version // Maybe we should add an option to customize it in future versions. async fn prompt_for_install( - handle: AppHandle, - updater: &self::core::Update, + update: &self::core::Update, app_name: &str, body: &str, pubkey: String, ) -> Result<()> { // remove single & double quote let escaped_body = body.replace(&['\"', '\''][..], ""); - let windows = handle.windows(); + let windows = update.app.windows(); let parent_window = windows.values().next(); // todo(lemarier): We should review this and make sure we have @@ -609,7 +726,7 @@ Would you like to install it now? Release Notes: {}"#, - app_name, updater.version, updater.current_version, escaped_body, + app_name, update.version, update.current_version, escaped_body, ), ); @@ -618,7 +735,9 @@ Release Notes: // macOS we display the `Ready to restart dialog` asking to restart // Windows is closing the current App and launch the downloaded MSI when ready (the process stop here) // Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here) - updater.download_and_install(pubkey.clone()).await?; + update + .download_and_install(pubkey.clone(), |_, _| ()) + .await?; // Ask user if we need to restart the application let should_exit = ask( @@ -627,7 +746,7 @@ Release Notes: "The installation was successful, do you want to restart the application now?", ); if should_exit { - handle.restart(); + update.app.restart(); } } diff --git a/core/tauri/tests/restart/Cargo.lock b/core/tauri/tests/restart/Cargo.lock index da596f21c98..54577883408 100644 --- a/core/tauri/tests/restart/Cargo.lock +++ b/core/tauri/tests/restart/Cargo.lock @@ -1596,9 +1596,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" [[package]] name = "pango" @@ -2601,6 +2601,7 @@ name = "tauri-runtime-wry" version = "0.3.3" dependencies = [ "gtk", + "rand 0.8.5", "tauri-runtime", "tauri-utils", "uuid", diff --git a/tooling/api/src/helpers/event.ts b/tooling/api/src/helpers/event.ts index 364aab6f0c4..1437cc7c6ab 100644 --- a/tooling/api/src/helpers/event.ts +++ b/tooling/api/src/helpers/event.ts @@ -21,6 +21,7 @@ export interface Event { export type EventName = LiteralUnion< | 'tauri://update' | 'tauri://update-available' + | 'tauri://update-download-progress' | 'tauri://update-install' | 'tauri://update-status' | 'tauri://resize'