feat(updater): add `download` and `install` js binding (#1330)

pull/1357/head
Tony 1 year ago committed by GitHub
parent f3749e4de8
commit 43224c5d5c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,6 @@
---
"updater": "patch"
"updater-js": "patch"
---
Add `Update.download` and `Update.install` functions to the JavaScript API

@ -5927,6 +5927,13 @@
"updater:allow-check" "updater:allow-check"
] ]
}, },
{
"description": "updater:allow-download -> Enables the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-download"
]
},
{ {
"description": "updater:allow-download-and-install -> Enables the download_and_install command without any pre-configured scope.", "description": "updater:allow-download-and-install -> Enables the download_and_install command without any pre-configured scope.",
"type": "string", "type": "string",
@ -5934,6 +5941,13 @@
"updater:allow-download-and-install" "updater:allow-download-and-install"
] ]
}, },
{
"description": "updater:allow-install -> Enables the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-install"
]
},
{ {
"description": "updater:deny-check -> Denies the check command without any pre-configured scope.", "description": "updater:deny-check -> Denies the check command without any pre-configured scope.",
"type": "string", "type": "string",
@ -5941,6 +5955,13 @@
"updater:deny-check" "updater:deny-check"
] ]
}, },
{
"description": "updater:deny-download -> Denies the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-download"
]
},
{ {
"description": "updater:deny-download-and-install -> Denies the download_and_install command without any pre-configured scope.", "description": "updater:deny-download-and-install -> Denies the download_and_install command without any pre-configured scope.",
"type": "string", "type": "string",
@ -5948,6 +5969,13 @@
"updater:deny-download-and-install" "updater:deny-download-and-install"
] ]
}, },
{
"description": "updater:deny-install -> Denies the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-install"
]
},
{ {
"description": "webview:default -> Default permissions for the plugin.", "description": "webview:default -> Default permissions for the plugin.",
"type": "string", "type": "string",

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_UPDATER__=function(e){"use strict";function t(e,t,s,r){if("a"===s&&!r)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!r:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===s?r:"a"===s?r.call(e):r?r.value:t.get(e)}function s(e,t,s,r,n){if("function"==typeof t?e!==t||!n:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return t.set(e,s),s}var r,n,i,a;"function"==typeof SuppressedError&&SuppressedError;class o{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,r.set(this,(()=>{})),n.set(this,0),i.set(this,{}),this.id=function(e,t=!1){return window.__TAURI_INTERNALS__.transformCallback(e,t)}((({message:e,id:a})=>{if(a===t(this,n,"f")){s(this,n,a+1),t(this,r,"f").call(this,e);const o=Object.keys(t(this,i,"f"));if(o.length>0){let e=a+1;for(const s of o.sort()){if(parseInt(s)!==e)break;{const n=t(this,i,"f")[s];delete t(this,i,"f")[s],t(this,r,"f").call(this,n),e+=1}}s(this,n,e)}}else t(this,i,"f")[a.toString()]=e}))}set onmessage(e){s(this,r,e)}get onmessage(){return t(this,r,"f")}toJSON(){return`__CHANNEL__:${this.id}`}}async function c(e,t={},s){return window.__TAURI_INTERNALS__.invoke(e,t,s)}r=new WeakMap,n=new WeakMap,i=new WeakMap;class d{get rid(){return t(this,a,"f")}constructor(e){a.set(this,void 0),s(this,a,e)}async close(){return c("plugin:resources|close",{rid:this.rid})}}a=new WeakMap;class h extends d{constructor(e){super(e.rid),this.available=e.available,this.currentVersion=e.currentVersion,this.version=e.version,this.date=e.date,this.body=e.body}async downloadAndInstall(e){const t=new o;e&&(t.onmessage=e),await c("plugin:updater|download_and_install",{onEvent:t,rid:this.rid})}}return e.Update=h,e.check=async function(e){return e?.headers&&(e.headers=Array.from(new Headers(e.headers).entries())),await c("plugin:updater|check",{...e}).then((e=>e.available?new h(e):null))},e}({});Object.defineProperty(window.__TAURI__,"updater",{value:__TAURI_PLUGIN_UPDATER__})} if("__TAURI__"in window){var __TAURI_PLUGIN_UPDATER__=function(e){"use strict";function t(e,t,s,n){if("a"===s&&!n)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!n:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===s?n:"a"===s?n.call(e):n?n.value:t.get(e)}function s(e,t,s,n,i){if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return t.set(e,s),s}var n,i,a,r;"function"==typeof SuppressedError&&SuppressedError;class o{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,n.set(this,(()=>{})),i.set(this,0),a.set(this,{}),this.id=function(e,t=!1){return window.__TAURI_INTERNALS__.transformCallback(e,t)}((({message:e,id:r})=>{if(r===t(this,i,"f")){s(this,i,r+1),t(this,n,"f").call(this,e);const o=Object.keys(t(this,a,"f"));if(o.length>0){let e=r+1;for(const s of o.sort()){if(parseInt(s)!==e)break;{const i=t(this,a,"f")[s];delete t(this,a,"f")[s],t(this,n,"f").call(this,i),e+=1}}s(this,i,e)}}else t(this,a,"f")[r.toString()]=e}))}set onmessage(e){s(this,n,e)}get onmessage(){return t(this,n,"f")}toJSON(){return`__CHANNEL__:${this.id}`}}async function d(e,t={},s){return window.__TAURI_INTERNALS__.invoke(e,t,s)}n=new WeakMap,i=new WeakMap,a=new WeakMap;class l{get rid(){return t(this,r,"f")}constructor(e){r.set(this,void 0),s(this,r,e)}async close(){return d("plugin:resources|close",{rid:this.rid})}}r=new WeakMap;class c extends l{constructor(e){super(e.rid),this.available=e.available,this.currentVersion=e.currentVersion,this.version=e.version,this.date=e.date,this.body=e.body}async download(e){const t=new o;e&&(t.onmessage=e);const s=await d("plugin:updater|download",{onEvent:t,rid:this.rid});this.downloadedBytes=new l(s)}async install(){if(!this.downloadedBytes)throw"Update.install called before Update.download";await d("plugin:updater|install",{updateRid:this.rid,bytesRid:this.downloadedBytes.rid}),this.downloadedBytes=void 0}async downloadAndInstall(e){const t=new o;e&&(t.onmessage=e),await d("plugin:updater|download_and_install",{onEvent:t,rid:this.rid})}async close(){await(this.downloadedBytes?.close()),await super.close()}}return e.Update=c,e.check=async function(e){return e?.headers&&(e.headers=Array.from(new Headers(e.headers).entries())),await d("plugin:updater|check",{...e}).then((e=>e.available?new c(e):null))},e}({});Object.defineProperty(window.__TAURI__,"updater",{value:__TAURI_PLUGIN_UPDATER__})}

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
const COMMANDS: &[&str] = &["check", "download_and_install"]; const COMMANDS: &[&str] = &["check", "download", "install", "download_and_install"];
fn main() { fn main() {
tauri_plugin::Builder::new(COMMANDS) tauri_plugin::Builder::new(COMMANDS)

@ -45,6 +45,7 @@ class Update extends Resource {
version: string; version: string;
date?: string; date?: string;
body?: string; body?: string;
private downloadedBytes?: Resource;
constructor(metadata: UpdateMetadata) { constructor(metadata: UpdateMetadata) {
super(metadata.rid); super(metadata.rid);
@ -55,6 +56,34 @@ class Update extends Resource {
this.body = metadata.body; this.body = metadata.body;
} }
/** Download the updater package */
async download(onEvent?: (progress: DownloadEvent) => void): Promise<void> {
const channel = new Channel<DownloadEvent>();
if (onEvent) {
channel.onmessage = onEvent;
}
const downloadedBytesRid = await invoke<number>("plugin:updater|download", {
onEvent: channel,
rid: this.rid,
});
this.downloadedBytes = new Resource(downloadedBytesRid);
}
/** Install downloaded updater package */
async install(): Promise<void> {
if (!this.downloadedBytes) {
throw "Update.install called before Update.download";
}
await invoke("plugin:updater|install", {
updateRid: this.rid,
bytesRid: this.downloadedBytes.rid,
});
// Don't need to call close, we did it in rust side already
this.downloadedBytes = undefined;
}
/** Downloads the updater package and installs it */ /** Downloads the updater package and installs it */
async downloadAndInstall( async downloadAndInstall(
onEvent?: (progress: DownloadEvent) => void, onEvent?: (progress: DownloadEvent) => void,
@ -68,6 +97,11 @@ class Update extends Resource {
rid: this.rid, rid: this.rid,
}); });
} }
async close(): Promise<void> {
await this.downloadedBytes?.close();
await super.close();
}
} }
/** Check for updates, resolves to `null` if no updates are available */ /** Check for updates, resolves to `null` if no updates are available */

@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-download"
description = "Enables the download command without any pre-configured scope."
commands.allow = ["download"]
[[permission]]
identifier = "deny-download"
description = "Denies the download command without any pre-configured scope."
commands.deny = ["download"]

@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-install"
description = "Enables the install command without any pre-configured scope."
commands.allow = ["install"]
[[permission]]
identifier = "deny-install"
description = "Denies the install command without any pre-configured scope."
commands.deny = ["install"]

@ -2,6 +2,10 @@
|------|-----| |------|-----|
|`allow-check`|Enables the check command without any pre-configured scope.| |`allow-check`|Enables the check command without any pre-configured scope.|
|`deny-check`|Denies the check command without any pre-configured scope.| |`deny-check`|Denies the check command without any pre-configured scope.|
|`allow-download`|Enables the download command without any pre-configured scope.|
|`deny-download`|Denies the download command without any pre-configured scope.|
|`allow-download-and-install`|Enables the download_and_install command without any pre-configured scope.| |`allow-download-and-install`|Enables the download_and_install command without any pre-configured scope.|
|`deny-download-and-install`|Denies the download_and_install command without any pre-configured scope.| |`deny-download-and-install`|Denies the download_and_install command without any pre-configured scope.|
|`allow-install`|Enables the install command without any pre-configured scope.|
|`deny-install`|Denies the install command without any pre-configured scope.|
|`default`|Allows checking for new updates and installing them| |`default`|Allows checking for new updates and installing them|

@ -1,4 +1,9 @@
"$schema" = "schemas/schema.json" "$schema" = "schemas/schema.json"
[default] [default]
description = "Allows checking for new updates and installing them" description = "Allows checking for new updates and installing them"
permissions = ["allow-check", "allow-download-and-install"] permissions = [
"allow-check",
"allow-download",
"allow-install",
"allow-download-and-install",
]

@ -308,6 +308,20 @@
"deny-check" "deny-check"
] ]
}, },
{
"description": "allow-download -> Enables the download command without any pre-configured scope.",
"type": "string",
"enum": [
"allow-download"
]
},
{
"description": "deny-download -> Denies the download command without any pre-configured scope.",
"type": "string",
"enum": [
"deny-download"
]
},
{ {
"description": "allow-download-and-install -> Enables the download_and_install command without any pre-configured scope.", "description": "allow-download-and-install -> Enables the download_and_install command without any pre-configured scope.",
"type": "string", "type": "string",
@ -322,6 +336,20 @@
"deny-download-and-install" "deny-download-and-install"
] ]
}, },
{
"description": "allow-install -> Enables the install command without any pre-configured scope.",
"type": "string",
"enum": [
"allow-install"
]
},
{
"description": "deny-install -> Denies the install command without any pre-configured scope.",
"type": "string",
"enum": [
"deny-install"
]
},
{ {
"description": "default -> Allows checking for new updates and installing them", "description": "default -> Allows checking for new updates and installing them",
"type": "string", "type": "string",

@ -5,7 +5,7 @@
use crate::{Result, Update, UpdaterExt}; use crate::{Result, Update, UpdaterExt};
use serde::Serialize; use serde::Serialize;
use tauri::{ipc::Channel, Manager, ResourceId, Runtime, Webview}; use tauri::{ipc::Channel, Manager, Resource, ResourceId, Runtime, Webview};
use std::time::Duration; use std::time::Duration;
use url::Url; use url::Url;
@ -35,6 +35,9 @@ pub(crate) struct Metadata {
body: Option<String>, body: Option<String>,
} }
struct DownloadedBytes(pub Vec<u8>);
impl Resource for DownloadedBytes {}
#[tauri::command] #[tauri::command]
pub(crate) async fn check<R: Runtime>( pub(crate) async fn check<R: Runtime>(
webview: Webview<R>, webview: Webview<R>,
@ -75,6 +78,46 @@ pub(crate) async fn check<R: Runtime>(
Ok(metadata) Ok(metadata)
} }
#[tauri::command]
pub(crate) async fn download<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
on_event: Channel,
) -> Result<ResourceId> {
let update = webview.resources_table().get::<Update>(rid)?;
let mut first_chunk = true;
let bytes = update
.download(
|chunk_length, content_length| {
if first_chunk {
first_chunk = !first_chunk;
let _ = on_event.send(DownloadEvent::Started { content_length });
}
let _ = on_event.send(DownloadEvent::Progress { chunk_length });
},
|| {
let _ = on_event.send(&DownloadEvent::Finished);
},
)
.await?;
Ok(webview.resources_table().add(DownloadedBytes(bytes)))
}
#[tauri::command]
pub(crate) async fn install<R: Runtime>(
webview: Webview<R>,
update_rid: ResourceId,
bytes_rid: ResourceId,
) -> Result<()> {
let update = webview.resources_table().get::<Update>(update_rid)?;
let bytes = webview
.resources_table()
.get::<DownloadedBytes>(bytes_rid)?;
update.install(&bytes.0)?;
let _ = webview.resources_table().close(bytes_rid);
Ok(())
}
#[tauri::command] #[tauri::command]
pub(crate) async fn download_and_install<R: Runtime>( pub(crate) async fn download_and_install<R: Runtime>(
webview: Webview<R>, webview: Webview<R>,

@ -179,7 +179,9 @@ impl Builder {
}) })
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
commands::check, commands::check,
commands::download_and_install commands::download,
commands::install,
commands::download_and_install,
]) ])
.build() .build()
} }

@ -5,7 +5,7 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
ffi::{OsStr, OsString}, ffi::{OsStr, OsString},
io::{Cursor, Read}, io::Cursor,
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr, str::FromStr,
sync::Arc, sync::Arc,
@ -478,19 +478,16 @@ impl Update {
on_chunk(chunk.len(), content_length); on_chunk(chunk.len(), content_length);
buffer.extend(chunk); buffer.extend(chunk);
} }
on_download_finish(); on_download_finish();
let mut update_buffer = Cursor::new(&buffer); verify_signature(&buffer, &self.signature, &self.config.pubkey)?;
verify_signature(&mut update_buffer, &self.signature, &self.config.pubkey)?;
Ok(buffer) Ok(buffer)
} }
/// Installs the updater package downloaded by [`Update::download`] /// Installs the updater package downloaded by [`Update::download`]
pub fn install(&self, bytes: Vec<u8>) -> Result<()> { pub fn install(&self, bytes: impl AsRef<[u8]>) -> Result<()> {
self.install_inner(bytes) self.install_inner(bytes.as_ref())
} }
/// Downloads and installs the updater package /// Downloads and installs the updater package
@ -504,7 +501,7 @@ impl Update {
} }
#[cfg(mobile)] #[cfg(mobile)]
fn install_inner(&self, _bytes: Vec<u8>) -> Result<()> { fn install_inner(&self, _bytes: &[u8]) -> Result<()> {
Ok(()) Ok(())
} }
} }
@ -546,13 +543,13 @@ impl Update {
/// ├── [AppName]_[version]_x64-setup.exe.zip # ZIP generated by tauri-bundler /// ├── [AppName]_[version]_x64-setup.exe.zip # ZIP generated by tauri-bundler
/// │ └──[AppName]_[version]_x64-setup.exe # NSIS installer /// │ └──[AppName]_[version]_x64-setup.exe # NSIS installer
/// └── ... /// └── ...
fn install_inner(&self, bytes: Vec<u8>) -> Result<()> { fn install_inner(&self, bytes: &[u8]) -> Result<()> {
use windows_sys::{ use windows_sys::{
w, w,
Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_SHOW}, Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_SHOW},
}; };
let (updater_type, path, _temp) = Self::extract(&bytes)?; let (updater_type, path, _temp) = Self::extract(bytes)?;
let install_mode = self.config.install_mode(); let install_mode = self.config.install_mode();
let mut installer_args = self.installer_args(); let mut installer_args = self.installer_args();
@ -666,7 +663,7 @@ impl Update {
/// We should have an AppImage already installed to be able to copy and install /// We should have an AppImage already installed to be able to copy and install
/// the extract_path is the current AppImage path /// the extract_path is the current AppImage path
/// tmp_dir is where our new AppImage is found /// tmp_dir is where our new AppImage is found
fn install_inner(&self, bytes: Vec<u8>) -> Result<()> { fn install_inner(&self, bytes: &[u8]) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::os::unix::fs::{MetadataExt, PermissionsExt};
let extract_path_metadata = self.extract_path.metadata()?; let extract_path_metadata = self.extract_path.metadata()?;
@ -694,7 +691,7 @@ impl Update {
std::fs::rename(&self.extract_path, tmp_app_image)?; std::fs::rename(&self.extract_path, tmp_app_image)?;
#[cfg(feature = "zip")] #[cfg(feature = "zip")]
if infer::archive::is_gz(&bytes) { if infer::archive::is_gz(bytes) {
// extract the buffer to the tmp_dir // extract the buffer to the tmp_dir
// we extract our signed archive into our final directory without any temp file // we extract our signed archive into our final directory without any temp file
let archive = Cursor::new(bytes); let archive = Cursor::new(bytes);
@ -743,7 +740,7 @@ impl Update {
/// │ └── Contents # Application contents... /// │ └── Contents # Application contents...
/// │ └── ... /// │ └── ...
/// └── ... /// └── ...
fn install_inner(&self, bytes: Vec<u8>) -> Result<()> { fn install_inner(&self, bytes: &[u8]) -> Result<()> {
use flate2::read::GzDecoder; use flate2::read::GzDecoder;
let cursor = Cursor::new(bytes); let cursor = Cursor::new(bytes);
@ -919,30 +916,15 @@ where
} }
// Validate signature // Validate signature
// need to be public because its been used fn verify_signature(data: &[u8], release_signature: &str, pub_key: &str) -> Result<bool> {
// by our tests in the bundler
//
// NOTE: The buffer position is not reset.
pub fn verify_signature<R>(
archive_reader: &mut R,
release_signature: &str,
pub_key: &str,
) -> Result<bool>
where
R: Read,
{
// we need to convert the pub key // we need to convert the pub key
let pub_key_decoded = base64_to_string(pub_key)?; let pub_key_decoded = base64_to_string(pub_key)?;
let public_key = PublicKey::decode(&pub_key_decoded)?; let public_key = PublicKey::decode(&pub_key_decoded)?;
let signature_base64_decoded = base64_to_string(release_signature)?; let signature_base64_decoded = base64_to_string(release_signature)?;
let signature = Signature::decode(&signature_base64_decoded)?; let signature = Signature::decode(&signature_base64_decoded)?;
// read all bytes until EOF in the buffer
let mut data = Vec::new();
archive_reader.read_to_end(&mut data)?;
// Validate signature or bail out // Validate signature or bail out
public_key.verify(&data, &signature, true)?; public_key.verify(data, &signature, true)?;
Ok(true) Ok(true)
} }

@ -40,6 +40,8 @@ fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTa
.args(["tauri", "build", "--debug", "--verbose"]) .args(["tauri", "build", "--debug", "--verbose"])
.arg("--config") .arg("--config")
.arg(serde_json::to_string(config).unwrap()) .arg(serde_json::to_string(config).unwrap())
.env("TAURI_SIGNING_PRIVATE_KEY", UPDATER_PRIVATE_KEY)
.env("TAURI_SIGNING_PRIVATE_KEY_PASSWORD", "")
.current_dir(cwd); .current_dir(cwd);
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@ -51,10 +53,7 @@ fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTa
#[cfg(windows)] #[cfg(windows)]
command.args(["--bundles", "msi", "nsis"]); command.args(["--bundles", "msi", "nsis"]);
command command.args(["--bundles", "updater"]);
.env("TAURI_SIGNING_PRIVATE_KEY", UPDATER_PRIVATE_KEY)
.env("TAURI_SIGNING_PRIVATE_KEY_PASSWORD", "")
.args(["--bundles", "updater"]);
} else { } else {
#[cfg(windows)] #[cfg(windows)]
command.args(["--bundles", target.name()]); command.args(["--bundles", target.name()]);
@ -64,7 +63,7 @@ fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTa
.status() .status()
.expect("failed to run Tauri CLI to bundle app"); .expect("failed to run Tauri CLI to bundle app");
if !status.code().map(|c| c == 0).unwrap_or(true) { if !status.success() {
panic!("failed to bundle app {:?}", status.code()); panic!("failed to bundle app {:?}", status.code());
} }
} }

Loading…
Cancel
Save