From 43224c5d5cfe2dd676e79ebafe424027c62c51c3 Mon Sep 17 00:00:00 2001 From: Tony <68118705+Legend-Master@users.noreply.github.com> Date: Tue, 21 May 2024 23:54:06 +0800 Subject: [PATCH] feat(updater): add `download` and `install` js binding (#1330) --- .../updater-download-install-js-binding.md | 6 +++ .../src-tauri/gen/schemas/desktop-schema.json | 28 ++++++++++++ plugins/updater/api-iife.js | 2 +- plugins/updater/build.rs | 2 +- plugins/updater/guest-js/index.ts | 34 ++++++++++++++ .../autogenerated/commands/download.toml | 13 ++++++ .../autogenerated/commands/install.toml | 13 ++++++ .../permissions/autogenerated/reference.md | 4 ++ plugins/updater/permissions/default.toml | 7 ++- .../updater/permissions/schemas/schema.json | 28 ++++++++++++ plugins/updater/src/commands.rs | 45 ++++++++++++++++++- plugins/updater/src/lib.rs | 4 +- plugins/updater/src/updater.rs | 42 +++++------------ .../updater/tests/app-updater/tests/update.rs | 9 ++-- 14 files changed, 197 insertions(+), 40 deletions(-) create mode 100644 .changes/updater-download-install-js-binding.md create mode 100644 plugins/updater/permissions/autogenerated/commands/download.toml create mode 100644 plugins/updater/permissions/autogenerated/commands/install.toml diff --git a/.changes/updater-download-install-js-binding.md b/.changes/updater-download-install-js-binding.md new file mode 100644 index 00000000..5bd0feb4 --- /dev/null +++ b/.changes/updater-download-install-js-binding.md @@ -0,0 +1,6 @@ +--- +"updater": "patch" +"updater-js": "patch" +--- + +Add `Update.download` and `Update.install` functions to the JavaScript API diff --git a/examples/api/src-tauri/gen/schemas/desktop-schema.json b/examples/api/src-tauri/gen/schemas/desktop-schema.json index c00d481b..81d3bd1c 100644 --- a/examples/api/src-tauri/gen/schemas/desktop-schema.json +++ b/examples/api/src-tauri/gen/schemas/desktop-schema.json @@ -5927,6 +5927,13 @@ "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.", "type": "string", @@ -5934,6 +5941,13 @@ "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.", "type": "string", @@ -5941,6 +5955,13 @@ "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.", "type": "string", @@ -5948,6 +5969,13 @@ "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.", "type": "string", diff --git a/plugins/updater/api-iife.js b/plugins/updater/api-iife.js index 597aeb3f..05651e75 100644 --- a/plugins/updater/api-iife.js +++ b/plugins/updater/api-iife.js @@ -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__})} diff --git a/plugins/updater/build.rs b/plugins/updater/build.rs index 8e3c3859..7c7774b9 100644 --- a/plugins/updater/build.rs +++ b/plugins/updater/build.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -const COMMANDS: &[&str] = &["check", "download_and_install"]; +const COMMANDS: &[&str] = &["check", "download", "install", "download_and_install"]; fn main() { tauri_plugin::Builder::new(COMMANDS) diff --git a/plugins/updater/guest-js/index.ts b/plugins/updater/guest-js/index.ts index 7d30638b..c5ce0735 100644 --- a/plugins/updater/guest-js/index.ts +++ b/plugins/updater/guest-js/index.ts @@ -45,6 +45,7 @@ class Update extends Resource { version: string; date?: string; body?: string; + private downloadedBytes?: Resource; constructor(metadata: UpdateMetadata) { super(metadata.rid); @@ -55,6 +56,34 @@ class Update extends Resource { this.body = metadata.body; } + /** Download the updater package */ + async download(onEvent?: (progress: DownloadEvent) => void): Promise { + const channel = new Channel(); + if (onEvent) { + channel.onmessage = onEvent; + } + const downloadedBytesRid = await invoke("plugin:updater|download", { + onEvent: channel, + rid: this.rid, + }); + this.downloadedBytes = new Resource(downloadedBytesRid); + } + + /** Install downloaded updater package */ + async install(): Promise { + 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 */ async downloadAndInstall( onEvent?: (progress: DownloadEvent) => void, @@ -68,6 +97,11 @@ class Update extends Resource { rid: this.rid, }); } + + async close(): Promise { + await this.downloadedBytes?.close(); + await super.close(); + } } /** Check for updates, resolves to `null` if no updates are available */ diff --git a/plugins/updater/permissions/autogenerated/commands/download.toml b/plugins/updater/permissions/autogenerated/commands/download.toml new file mode 100644 index 00000000..896b30ce --- /dev/null +++ b/plugins/updater/permissions/autogenerated/commands/download.toml @@ -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"] diff --git a/plugins/updater/permissions/autogenerated/commands/install.toml b/plugins/updater/permissions/autogenerated/commands/install.toml new file mode 100644 index 00000000..4c6a29d4 --- /dev/null +++ b/plugins/updater/permissions/autogenerated/commands/install.toml @@ -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"] diff --git a/plugins/updater/permissions/autogenerated/reference.md b/plugins/updater/permissions/autogenerated/reference.md index 2dacf032..e9991380 100644 --- a/plugins/updater/permissions/autogenerated/reference.md +++ b/plugins/updater/permissions/autogenerated/reference.md @@ -2,6 +2,10 @@ |------|-----| |`allow-check`|Enables 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.| |`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| diff --git a/plugins/updater/permissions/default.toml b/plugins/updater/permissions/default.toml index 857f9b5e..ea994115 100644 --- a/plugins/updater/permissions/default.toml +++ b/plugins/updater/permissions/default.toml @@ -1,4 +1,9 @@ "$schema" = "schemas/schema.json" [default] 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", +] diff --git a/plugins/updater/permissions/schemas/schema.json b/plugins/updater/permissions/schemas/schema.json index 69135976..4d03368e 100644 --- a/plugins/updater/permissions/schemas/schema.json +++ b/plugins/updater/permissions/schemas/schema.json @@ -308,6 +308,20 @@ "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.", "type": "string", @@ -322,6 +336,20 @@ "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", "type": "string", diff --git a/plugins/updater/src/commands.rs b/plugins/updater/src/commands.rs index f49caca8..a6592ab9 100644 --- a/plugins/updater/src/commands.rs +++ b/plugins/updater/src/commands.rs @@ -5,7 +5,7 @@ use crate::{Result, Update, UpdaterExt}; 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 url::Url; @@ -35,6 +35,9 @@ pub(crate) struct Metadata { body: Option, } +struct DownloadedBytes(pub Vec); +impl Resource for DownloadedBytes {} + #[tauri::command] pub(crate) async fn check( webview: Webview, @@ -75,6 +78,46 @@ pub(crate) async fn check( Ok(metadata) } +#[tauri::command] +pub(crate) async fn download( + webview: Webview, + rid: ResourceId, + on_event: Channel, +) -> Result { + let update = webview.resources_table().get::(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( + webview: Webview, + update_rid: ResourceId, + bytes_rid: ResourceId, +) -> Result<()> { + let update = webview.resources_table().get::(update_rid)?; + let bytes = webview + .resources_table() + .get::(bytes_rid)?; + update.install(&bytes.0)?; + let _ = webview.resources_table().close(bytes_rid); + Ok(()) +} + #[tauri::command] pub(crate) async fn download_and_install( webview: Webview, diff --git a/plugins/updater/src/lib.rs b/plugins/updater/src/lib.rs index 5ddfe8ed..327ca7ad 100644 --- a/plugins/updater/src/lib.rs +++ b/plugins/updater/src/lib.rs @@ -179,7 +179,9 @@ impl Builder { }) .invoke_handler(tauri::generate_handler![ commands::check, - commands::download_and_install + commands::download, + commands::install, + commands::download_and_install, ]) .build() } diff --git a/plugins/updater/src/updater.rs b/plugins/updater/src/updater.rs index 916a04d8..e800a45c 100644 --- a/plugins/updater/src/updater.rs +++ b/plugins/updater/src/updater.rs @@ -5,7 +5,7 @@ use std::{ collections::HashMap, ffi::{OsStr, OsString}, - io::{Cursor, Read}, + io::Cursor, path::{Path, PathBuf}, str::FromStr, sync::Arc, @@ -478,19 +478,16 @@ impl Update { on_chunk(chunk.len(), content_length); buffer.extend(chunk); } - on_download_finish(); - let mut update_buffer = Cursor::new(&buffer); - - verify_signature(&mut update_buffer, &self.signature, &self.config.pubkey)?; + verify_signature(&buffer, &self.signature, &self.config.pubkey)?; Ok(buffer) } /// Installs the updater package downloaded by [`Update::download`] - pub fn install(&self, bytes: Vec) -> Result<()> { - self.install_inner(bytes) + pub fn install(&self, bytes: impl AsRef<[u8]>) -> Result<()> { + self.install_inner(bytes.as_ref()) } /// Downloads and installs the updater package @@ -504,7 +501,7 @@ impl Update { } #[cfg(mobile)] - fn install_inner(&self, _bytes: Vec) -> Result<()> { + fn install_inner(&self, _bytes: &[u8]) -> Result<()> { Ok(()) } } @@ -546,13 +543,13 @@ impl Update { /// ├── [AppName]_[version]_x64-setup.exe.zip # ZIP generated by tauri-bundler /// │ └──[AppName]_[version]_x64-setup.exe # NSIS installer /// └── ... - fn install_inner(&self, bytes: Vec) -> Result<()> { + fn install_inner(&self, bytes: &[u8]) -> Result<()> { use windows_sys::{ w, 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 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 /// the extract_path is the current AppImage path /// tmp_dir is where our new AppImage is found - fn install_inner(&self, bytes: Vec) -> Result<()> { + fn install_inner(&self, bytes: &[u8]) -> Result<()> { use std::os::unix::fs::{MetadataExt, PermissionsExt}; let extract_path_metadata = self.extract_path.metadata()?; @@ -694,7 +691,7 @@ impl Update { std::fs::rename(&self.extract_path, tmp_app_image)?; #[cfg(feature = "zip")] - if infer::archive::is_gz(&bytes) { + if infer::archive::is_gz(bytes) { // extract the buffer to the tmp_dir // we extract our signed archive into our final directory without any temp file let archive = Cursor::new(bytes); @@ -743,7 +740,7 @@ impl Update { /// │ └── Contents # Application contents... /// │ └── ... /// └── ... - fn install_inner(&self, bytes: Vec) -> Result<()> { + fn install_inner(&self, bytes: &[u8]) -> Result<()> { use flate2::read::GzDecoder; let cursor = Cursor::new(bytes); @@ -919,30 +916,15 @@ where } // Validate signature -// need to be public because its been used -// by our tests in the bundler -// -// NOTE: The buffer position is not reset. -pub fn verify_signature( - archive_reader: &mut R, - release_signature: &str, - pub_key: &str, -) -> Result -where - R: Read, -{ +fn verify_signature(data: &[u8], release_signature: &str, pub_key: &str) -> Result { // we need to convert the pub key let pub_key_decoded = base64_to_string(pub_key)?; let public_key = PublicKey::decode(&pub_key_decoded)?; let signature_base64_decoded = base64_to_string(release_signature)?; 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 - public_key.verify(&data, &signature, true)?; + public_key.verify(data, &signature, true)?; Ok(true) } diff --git a/plugins/updater/tests/app-updater/tests/update.rs b/plugins/updater/tests/app-updater/tests/update.rs index 9b6b520e..b6bd76a3 100644 --- a/plugins/updater/tests/app-updater/tests/update.rs +++ b/plugins/updater/tests/app-updater/tests/update.rs @@ -40,6 +40,8 @@ fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTa .args(["tauri", "build", "--debug", "--verbose"]) .arg("--config") .arg(serde_json::to_string(config).unwrap()) + .env("TAURI_SIGNING_PRIVATE_KEY", UPDATER_PRIVATE_KEY) + .env("TAURI_SIGNING_PRIVATE_KEY_PASSWORD", "") .current_dir(cwd); #[cfg(target_os = "linux")] @@ -51,10 +53,7 @@ fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTa #[cfg(windows)] command.args(["--bundles", "msi", "nsis"]); - command - .env("TAURI_SIGNING_PRIVATE_KEY", UPDATER_PRIVATE_KEY) - .env("TAURI_SIGNING_PRIVATE_KEY_PASSWORD", "") - .args(["--bundles", "updater"]); + command.args(["--bundles", "updater"]); } else { #[cfg(windows)] command.args(["--bundles", target.name()]); @@ -64,7 +63,7 @@ fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTa .status() .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()); } }