mod core;
mod error;
use std::time::Duration;
use http::header::{HeaderName, HeaderValue};
use semver::Version;
use time::OffsetDateTime;
pub use self::{core::RemoteRelease, error::Error};
pub type Result<T> = std::result::Result<T, Error>;
use crate::{runtime::EventLoopProxy, AppHandle, EventLoopMessage, Manager, Runtime, UpdaterEvent};
#[cfg(desktop)]
use crate::api::dialog::blocking::ask;
#[cfg(mobile)]
fn ask<R: Runtime>(
_parent_window: Option<&crate::Window<R>>,
_title: impl AsRef<str>,
_message: impl AsRef<str>,
) -> bool {
true
}
pub const EVENT_CHECK_UPDATE: &str = "tauri://update";
pub const EVENT_UPDATE_AVAILABLE: &str = "tauri://update-available";
pub const EVENT_INSTALL_UPDATE: &str = "tauri://update-install";
pub const EVENT_STATUS_UPDATE: &str = "tauri://update-status";
pub const EVENT_DOWNLOAD_PROGRESS: &str = "tauri://update-download-progress";
pub const EVENT_STATUS_PENDING: &str = "PENDING";
pub const EVENT_STATUS_ERROR: &str = "ERROR";
pub const EVENT_STATUS_DOWNLOADED: &str = "DOWNLOADED";
pub const EVENT_STATUS_SUCCESS: &str = "DONE";
pub const EVENT_STATUS_UPTODATE: &str = "UPTODATE";
pub fn target() -> Option<String> {
if let (Some(target), Some(arch)) = (core::get_updater_target(), core::get_updater_arch()) {
Some(format!("{target}-{arch}"))
} else {
None
}
}
#[derive(Clone, serde::Serialize)]
struct StatusEvent {
status: String,
error: Option<String>,
}
#[derive(Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct DownloadProgressEvent {
chunk_length: usize,
content_length: Option<u64>,
}
#[derive(Clone, serde::Serialize)]
struct UpdateManifest {
version: String,
date: Option<String>,
body: String,
}
#[derive(Debug)]
pub struct UpdateBuilder<R: Runtime> {
inner: core::UpdateBuilder<R>,
events: bool,
}
impl<R: Runtime> UpdateBuilder<R> {
pub fn skip_events(mut self) -> Self {
self.events = false;
self
}
pub fn target(mut self, target: impl Into<String>) -> Self {
self.inner = self.inner.target(target);
self
}
pub fn should_install<F: FnOnce(&Version, &RemoteRelease) -> bool + Send + 'static>(
mut self,
f: F,
) -> Self {
self.inner = self.inner.should_install(f);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.inner = self.inner.timeout(timeout);
self
}
pub fn header<K, V>(mut self, key: K, value: V) -> Result<Self>
where
HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<http::Error>,
HeaderValue: TryFrom<V>,
<HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
{
self.inner = self.inner.header(key, value)?;
Ok(self)
}
pub fn endpoints(mut self, urls: &[String]) -> Self {
self.inner = self.inner.urls(urls);
self
}
pub async fn check(self) -> Result<UpdateResponse<R>> {
let handle = self.inner.app.clone();
match self.inner.build().await {
Ok(update) => {
if self.events {
if update.should_update {
let body = update.body.clone().unwrap_or_else(|| String::from(""));
let _ = handle.emit_all(
EVENT_UPDATE_AVAILABLE,
UpdateManifest {
body: body.clone(),
date: update.date.map(|d| d.to_string()),
version: update.version.clone(),
},
);
let _ = handle.create_proxy().send_event(EventLoopMessage::Updater(
UpdaterEvent::UpdateAvailable {
body,
date: update.date,
version: update.version.clone(),
},
));
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,
match e {
Error::UpToDate => UpdaterEvent::AlreadyUpToDate,
_ => UpdaterEvent::Error(e.to_string()),
},
);
}
Err(e)
}
}
}
}
pub struct UpdateResponse<R: Runtime> {
update: core::Update<R>,
}
impl<R: Runtime> Clone for UpdateResponse<R> {
fn clone(&self) -> Self {
Self {
update: self.update.clone(),
}
}
}
impl<R: Runtime> UpdateResponse<R> {
pub fn is_update_available(&self) -> bool {
self.update.should_update
}
pub fn current_version(&self) -> &Version {
&self.update.current_version
}
pub fn latest_version(&self) -> &str {
&self.update.version
}
pub fn date(&self) -> Option<&OffsetDateTime> {
self.update.date.as_ref()
}
pub fn body(&self) -> Option<&String> {
self.update.body.as_ref()
}
pub fn header<K, V>(mut self, key: K, value: V) -> Result<Self>
where
HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<http::Error>,
HeaderValue: TryFrom<V>,
<HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
{
self.update = self.update.header(key, value)?;
Ok(self)
}
pub fn remove_header<K>(mut self, key: K) -> Result<Self>
where
HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<http::Error>,
{
self.update = self.update.remove_header(key)?;
Ok(self)
}
pub async fn download_and_install(self) -> Result<()> {
download_and_install(self.update).await
}
}
pub(crate) async fn check_update_with_dialog<R: Runtime>(handle: AppHandle<R>) {
let updater_config = handle.config().tauri.updater.clone();
let package_info = handle.package_info().clone();
if let Some(endpoints) = &updater_config.endpoints {
let endpoints = endpoints
.iter()
.map(|e| e.to_string())
.collect::<Vec<String>>();
let mut builder = self::core::builder(handle.clone())
.urls(&endpoints[..])
.current_version(package_info.version);
if let Some(target) = &handle.updater_settings.target {
builder = builder.target(target);
}
match builder.build().await {
Ok(updater) => {
let pubkey = updater_config.pubkey.clone();
if !updater.should_update {
send_status_update(&handle, UpdaterEvent::AlreadyUpToDate);
} else if updater_config.dialog {
let body = updater.body.clone().unwrap_or_else(|| String::from(""));
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()));
}
}
}
Err(e) => {
send_status_update(
&handle,
match e {
Error::UpToDate => UpdaterEvent::AlreadyUpToDate,
_ => UpdaterEvent::Error(e.to_string()),
},
);
}
}
}
}
pub(crate) fn listener<R: Runtime>(handle: AppHandle<R>) {
let handle_ = handle.clone();
handle.listen_global(EVENT_CHECK_UPDATE, move |_msg| {
let handle_ = handle_.clone();
crate::async_runtime::spawn(async move {
let _ = builder(handle_.clone()).check().await;
});
});
}
pub(crate) async fn download_and_install<R: Runtime>(update: core::Update<R>) -> Result<()> {
send_status_update(&update.app, UpdaterEvent::Pending);
let handle = update.app.clone();
let handle_ = handle.clone();
let update_result = update
.download_and_install(
update.app.config().tauri.updater.pubkey.clone(),
move |chunk_length, content_length| {
send_download_progress_event(&handle, chunk_length, content_length);
},
move || {
send_status_update(&handle_, UpdaterEvent::Downloaded);
},
)
.await;
if let Err(err) = &update_result {
send_status_update(&update.app, UpdaterEvent::Error(err.to_string()));
} else {
send_status_update(&update.app, UpdaterEvent::Updated);
}
update_result
}
pub fn builder<R: Runtime>(handle: AppHandle<R>) -> UpdateBuilder<R> {
let updater_config = &handle.config().tauri.updater;
let package_info = handle.package_info().clone();
let endpoints: Vec<String> = updater_config
.endpoints
.as_ref()
.map(|endpoints| endpoints.iter().map(|e| e.to_string()).collect())
.unwrap_or_default();
let mut builder = self::core::builder(handle.clone())
.urls(&endpoints[..])
.current_version(package_info.version);
if let Some(target) = &handle.updater_settings.target {
builder = builder.target(target);
}
UpdateBuilder {
inner: builder,
events: true,
}
}
fn send_download_progress_event<R: Runtime>(
handle: &AppHandle<R>,
chunk_length: usize,
content_length: Option<u64>,
) {
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,
}));
}
fn send_status_update<R: Runtime>(handle: &AppHandle<R>, message: UpdaterEvent) {
let _ = handle.emit_all(
EVENT_STATUS_UPDATE,
if let UpdaterEvent::Error(error) = &message {
StatusEvent {
error: Some(error.clone()),
status: message.clone().status_message().into(),
}
} else {
StatusEvent {
error: None,
status: message.clone().status_message().into(),
}
},
);
let _ = handle
.create_proxy()
.send_event(EventLoopMessage::Updater(message));
}
async fn prompt_for_install<R: Runtime>(
update: &self::core::Update<R>,
app_name: &str,
body: &str,
pubkey: String,
) -> Result<()> {
let windows = update.app.windows();
let parent_window = windows.values().next();
let should_install = ask(
parent_window,
format!(r#"A new version of {app_name} is available! "#),
format!(
r#"{app_name} {} is now available -- you have {}.
Would you like to install it now?
Release Notes:
{body}"#,
update.version, update.current_version
),
);
if should_install {
let handle = update.app.clone();
let handle_ = handle.clone();
update
.download_and_install(
pubkey.clone(),
move |chunk_length, content_length| {
send_download_progress_event(&handle, chunk_length, content_length);
},
move || {
send_status_update(&handle_, UpdaterEvent::Downloaded);
},
)
.await?;
send_status_update(&update.app, UpdaterEvent::Updated);
let should_exit = ask(
parent_window,
"Ready to Restart",
"The installation was successful, do you want to restart the application now?",
);
if should_exit {
update.app.restart();
}
}
Ok(())
}