Преглед на файлове

feat(updater): expose builder, allow setting a custom version checker (#3792)

Lucas Fernandes Nogueira преди 3 години
родител
ревизия
c64268f927
променени са 5 файла, в които са добавени 178 реда и са изтрити 69 реда
  1. 1 1
      .changes/updater-check-api.md
  2. 5 0
      .changes/updater-custom-version-checker.md
  3. 19 4
      core/tauri/src/app.rs
  4. 36 19
      core/tauri/src/updater/core.rs
  5. 117 45
      core/tauri/src/updater/mod.rs

+ 1 - 1
.changes/updater-check-api.md

@@ -2,4 +2,4 @@
 "tauri": patch
 ---
 
-Added `check_for_updates` method to `App` and `AppHandle`.
+Added `updater` method to `App` and `AppHandle`, a builder to check for app updates.

+ 5 - 0
.changes/updater-custom-version-checker.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Allow using a custom updater version checker via `App::updater().should_install()`.

+ 19 - 4
core/tauri/src/app.rs

@@ -398,10 +398,25 @@ macro_rules! shared_app_impl {
     impl<R: Runtime> $app {
       #[cfg(updater)]
       #[cfg_attr(doc_cfg, doc(cfg(feature = "updater")))]
-      /// Runs the updater to check if there is a new app version.
-      /// It is the same as triggering the `tauri://update` event.
-      pub async fn check_for_updates(&self) -> updater::Result<updater::UpdateResponse<R>> {
-        updater::check(self.app_handle()).await
+      /// Gets the updater builder to manually check if an update is available.
+      ///
+      /// # Examples
+      ///
+      /// ```no_run
+      /// tauri::Builder::default()
+      ///   .setup(|app| {
+      ///     let handle = app.handle();
+      ///     tauri::async_runtime::spawn(async move {
+      #[cfg_attr(
+        any(feature = "updater", feature = "__updater-docs"),
+        doc = r#"     let response = handle.updater().check().await;"#
+      )]
+      ///     });
+      ///     Ok(())
+      ///   });
+      /// ```
+      pub fn updater(&self) -> updater::UpdateBuilder<R> {
+        updater::builder(self.app_handle())
       }
 
       /// Creates a new webview window.

+ 36 - 19
core/tauri/src/updater/core.rs

@@ -21,7 +21,7 @@ use tauri_utils::{platform::current_exe, Env};
 use std::io::Seek;
 use std::{
   collections::HashMap,
-  env,
+  env, fmt,
   io::{Cursor, Read},
   path::{Path, PathBuf},
   str::from_utf8,
@@ -198,29 +198,42 @@ impl RemoteRelease {
   }
 }
 
-#[derive(Debug)]
-pub struct UpdateBuilder<'a, R: Runtime> {
+pub struct UpdateBuilder<R: Runtime> {
   /// Application handle.
   pub app: AppHandle<R>,
   /// Current version we are running to compare with announced version
-  pub current_version: &'a str,
+  pub current_version: String,
   /// The URLs to checks updates. We suggest at least one fallback on a different domain.
   pub urls: Vec<String>,
   /// The platform the updater will check and install the update. Default is from `get_updater_target`
   pub target: Option<String>,
   /// The current executable path. Default is automatically extracted.
   pub executable_path: Option<PathBuf>,
+  should_install: Option<Box<dyn FnOnce(&str, &str) -> bool + Send>>,
+}
+
+impl<R: Runtime> fmt::Debug for UpdateBuilder<R> {
+  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+    f.debug_struct("UpdateBuilder")
+      .field("app", &self.app)
+      .field("current_version", &self.current_version)
+      .field("urls", &self.urls)
+      .field("target", &self.target)
+      .field("executable_path", &self.executable_path)
+      .finish()
+  }
 }
 
 // Create new updater instance and return an Update
-impl<'a, R: Runtime> UpdateBuilder<'a, R> {
+impl<R: Runtime> UpdateBuilder<R> {
   pub fn new(app: AppHandle<R>) -> Self {
     UpdateBuilder {
       app,
       urls: Vec::new(),
       target: None,
       executable_path: None,
-      current_version: env!("CARGO_PKG_VERSION"),
+      current_version: env!("CARGO_PKG_VERSION").into(),
+      should_install: None,
     }
   }
 
@@ -250,15 +263,14 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> {
 
   /// Set the current app version, used to compare against the latest available version.
   /// The `cargo_crate_version!` macro can be used to pull the version from your `Cargo.toml`
-  pub fn current_version(mut self, ver: &'a str) -> Self {
-    self.current_version = ver;
+  pub fn current_version(mut self, ver: impl Into<String>) -> Self {
+    self.current_version = ver.into();
     self
   }
 
   /// Set the target name. Represents the string that is looked up on the updater API or response JSON.
-  #[allow(dead_code)]
-  pub fn target(mut self, target: &str) -> Self {
-    self.target = Some(target.to_owned());
+  pub fn target(mut self, target: impl Into<String>) -> Self {
+    self.target.replace(target.into());
     self
   }
 
@@ -269,7 +281,12 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> {
     self
   }
 
-  pub async fn build(self) -> Result<Update<R>> {
+  pub fn should_install<F: FnOnce(&str, &str) -> bool + Send + 'static>(mut self, f: F) -> Self {
+    self.should_install.replace(Box::new(f));
+    self
+  }
+
+  pub async fn build(mut self) -> Result<Update<R>> {
     let mut remote_release: Option<RemoteRelease> = None;
 
     // make sure we have at least one url
@@ -279,9 +296,6 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> {
       ));
     };
 
-    // set current version if not set
-    let current_version = self.current_version;
-
     // If no executable path provided, we use current_exe from tauri_utils
     let executable_path = self.executable_path.unwrap_or(current_exe()?);
 
@@ -324,7 +338,7 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> {
       // The main objective is if the update URL is defined via the Cargo.toml
       // the URL will be generated dynamicly
       let fixed_link = url
-        .replace("{{current_version}}", current_version)
+        .replace("{{current_version}}", &self.current_version)
         .replace("{{target}}", &target)
         .replace("{{arch}}", arch);
 
@@ -383,8 +397,11 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> {
     let final_release = remote_release.ok_or(Error::ReleaseNotFound)?;
 
     // did the announced version is greated than our current one?
-    let should_update =
-      version::is_greater(current_version, &final_release.version).unwrap_or(false);
+    let should_update = if let Some(comparator) = self.should_install.take() {
+      comparator(&self.current_version, &final_release.version)
+    } else {
+      version::is_greater(&self.current_version, &final_release.version).unwrap_or(false)
+    };
 
     // create our new updater
     Ok(Update {
@@ -404,7 +421,7 @@ impl<'a, R: Runtime> UpdateBuilder<'a, R> {
   }
 }
 
-pub fn builder<'a, R: Runtime>(app: AppHandle<R>) -> UpdateBuilder<'a, R> {
+pub fn builder<R: Runtime>(app: AppHandle<R>) -> UpdateBuilder<R> {
   UpdateBuilder::new(app)
 }
 

+ 117 - 45
core/tauri/src/updater/mod.rs

@@ -101,7 +101,7 @@
 //!   .setup(|app| {
 //!     let handle = app.handle();
 //!     tauri::async_runtime::spawn(async move {
-//!       let response = handle.check_for_updates().await;
+//!       let response = handle.updater().check().await;
 //!     });
 //!     Ok(())
 //!   });
@@ -165,7 +165,7 @@
 //!   .setup(|app| {
 //!     let handle = app.handle();
 //!     tauri::async_runtime::spawn(async move {
-//!       match handle.check_for_updates().await {
+//!       match handle.updater().check().await {
 //!         Ok(update) => {
 //!           if update.is_update_available() {
 //!             update.download_and_install().await.unwrap();
@@ -499,6 +499,115 @@ struct UpdateManifest {
   body: String,
 }
 
+/// An update check builder.
+#[derive(Debug)]
+pub struct UpdateBuilder<R: Runtime> {
+  inner: core::UpdateBuilder<R>,
+  events: bool,
+}
+
+impl<R: Runtime> UpdateBuilder<R> {
+  /// Do not use the event system to emit information or listen to install the update.
+  pub fn skip_events(mut self) -> Self {
+    self.events = false;
+    self
+  }
+
+  /// Set the target name. Represents the string that is looked up on the updater API or response JSON.
+  pub fn target(mut self, target: impl Into<String>) -> Self {
+    self.inner = self.inner.target(target);
+    self
+  }
+
+  /// Sets a closure that is invoked to compare the current version and the latest version returned by the updater server.
+  /// The first argument is the current version, and the second one is the latest version.
+  ///
+  /// The closure must return `true` if the update should be installed.
+  ///
+  /// # Examples
+  ///
+  /// - Always install the version returned by the server:
+  ///
+  /// ```no_run
+  /// tauri::Builder::default()
+  ///   .setup(|app| {
+  ///     tauri::updater::builder(app.handle()).should_install(|_current, _latest| true);
+  ///     Ok(())
+  ///   });
+  /// ```
+  pub fn should_install<F: FnOnce(&str, &str) -> bool + Send + 'static>(mut self, f: F) -> Self {
+    self.inner = self.inner.should_install(f);
+    self
+  }
+
+  /// Check if an update is available.
+  ///
+  /// # Examples
+  ///
+  /// ```no_run
+  /// tauri::Builder::default()
+  ///   .setup(|app| {
+  ///     let handle = app.handle();
+  ///     tauri::async_runtime::spawn(async move {
+  ///       match tauri::updater::builder(handle).check().await {
+  ///         Ok(update) => {}
+  ///         Err(error) => {}
+  ///       }
+  ///     });
+  ///     Ok(())
+  ///   });
+  /// ```
+  pub async fn check(self) -> Result<UpdateResponse<R>> {
+    let handle = self.inner.app.clone();
+    let events = self.events;
+    // check updates
+    match self.inner.build().await {
+      Ok(update) => {
+        if events {
+          // send notification if we need to update
+          if update.should_update {
+            let body = update.body.clone().unwrap_or_else(|| String::from(""));
+
+            // Emit `tauri://update-available`
+            let _ = handle.emit_all(
+              EVENT_UPDATE_AVAILABLE,
+              UpdateManifest {
+                body: body.clone(),
+                date: update.date.clone(),
+                version: update.version.clone(),
+              },
+            );
+            let _ = handle.create_proxy().send_event(EventLoopMessage::Updater(
+              UpdaterEvent::UpdateAvailable {
+                body,
+                date: update.date.clone(),
+                version: update.version.clone(),
+              },
+            ));
+
+            // Listen for `tauri://update-install`
+            let update_ = update.clone();
+            handle.once_global(EVENT_INSTALL_UPDATE, move |_msg| {
+              crate::async_runtime::spawn(async move {
+                let _ = download_and_install(update_).await;
+              });
+            });
+          } else {
+            send_status_update(&handle, UpdaterEvent::AlreadyUpToDate);
+          }
+        }
+        Ok(UpdateResponse { update })
+      }
+      Err(e) => {
+        if self.events {
+          send_status_update(&handle, UpdaterEvent::Error(e.to_string()));
+        }
+        Err(e)
+      }
+    }
+  }
+}
+
 /// The response of an updater check.
 pub struct UpdateResponse<R: Runtime> {
   update: core::Update<R>,
@@ -582,7 +691,7 @@ pub(crate) fn listener<R: Runtime>(handle: AppHandle<R>) {
   handle.listen_global(EVENT_CHECK_UPDATE, move |_msg| {
     let handle_ = handle_.clone();
     crate::async_runtime::spawn(async move {
-      let _ = check(handle_.clone()).await;
+      let _ = builder(handle_.clone()).check().await;
     });
   });
 }
@@ -617,7 +726,8 @@ pub(crate) async fn download_and_install<R: Runtime>(update: core::Update<R>) ->
   update_result
 }
 
-pub(crate) async fn check<R: Runtime>(handle: AppHandle<R>) -> Result<UpdateResponse<R>> {
+/// Initializes the [`UpdateBuilder`] using the app configuration.
+pub fn builder<R: Runtime>(handle: AppHandle<R>) -> UpdateBuilder<R> {
   let updater_config = &handle.config().tauri.updater;
   let package_info = handle.package_info().clone();
 
@@ -636,47 +746,9 @@ pub(crate) async fn check<R: Runtime>(handle: AppHandle<R>) -> Result<UpdateResp
   if let Some(target) = &handle.updater_settings.target {
     builder = builder.target(target);
   }
-
-  // check updates
-  match builder.build().await {
-    Ok(update) => {
-      // send notification if we need to update
-      if update.should_update {
-        let body = update.body.clone().unwrap_or_else(|| String::from(""));
-
-        // Emit `tauri://update-available`
-        let _ = handle.emit_all(
-          EVENT_UPDATE_AVAILABLE,
-          UpdateManifest {
-            body: body.clone(),
-            date: update.date.clone(),
-            version: update.version.clone(),
-          },
-        );
-        let _ = handle.create_proxy().send_event(EventLoopMessage::Updater(
-          UpdaterEvent::UpdateAvailable {
-            body,
-            date: update.date.clone(),
-            version: update.version.clone(),
-          },
-        ));
-
-        // Listen for `tauri://update-install`
-        let update_ = update.clone();
-        handle.once_global(EVENT_INSTALL_UPDATE, move |_msg| {
-          crate::async_runtime::spawn(async move {
-            let _ = download_and_install(update_).await;
-          });
-        });
-      } else {
-        send_status_update(&handle, UpdaterEvent::AlreadyUpToDate);
-      }
-      Ok(UpdateResponse { update })
-    }
-    Err(e) => {
-      send_status_update(&handle, UpdaterEvent::Error(e.to_string()));
-      Err(e)
-    }
+  UpdateBuilder {
+    inner: builder,
+    events: true,
   }
 }