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"
]
},
{
"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",

@ -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: MIT
const COMMANDS: &[&str] = &["check", "download_and_install"];
const COMMANDS: &[&str] = &["check", "download", "install", "download_and_install"];
fn main() {
tauri_plugin::Builder::new(COMMANDS)

@ -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<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 */
async downloadAndInstall(
onEvent?: (progress: DownloadEvent) => void,
@ -68,6 +97,11 @@ class Update extends Resource {
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 */

@ -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.|
|`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|

@ -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",
]

@ -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",

@ -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<String>,
}
struct DownloadedBytes(pub Vec<u8>);
impl Resource for DownloadedBytes {}
#[tauri::command]
pub(crate) async fn check<R: Runtime>(
webview: Webview<R>,
@ -75,6 +78,46 @@ pub(crate) async fn check<R: Runtime>(
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]
pub(crate) async fn download_and_install<R: Runtime>(
webview: Webview<R>,

@ -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()
}

@ -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<u8>) -> 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<u8>) -> 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<u8>) -> 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<u8>) -> 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<u8>) -> 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<R>(
archive_reader: &mut R,
release_signature: &str,
pub_key: &str,
) -> Result<bool>
where
R: Read,
{
fn verify_signature(data: &[u8], release_signature: &str, pub_key: &str) -> Result<bool> {
// 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)
}

@ -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());
}
}

Loading…
Cancel
Save