mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-05-09 12:36:07 +02:00
feat(updater): add download and install js binding (#1330)
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user