mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-07-22 17:10:53 +02:00
Compare commits
4 Commits
dialog-js-v2.7.2
...
v2
| Author | SHA1 | Date | |
|---|---|---|---|
| ab7489c964 | |||
| 3fb27bf13a | |||
| 7e45774610 | |||
| 526726162f |
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"deep-link": patch
|
||||
"deep-link-js": patch
|
||||
---
|
||||
|
||||
adds proper Platform-specific sections to the docs for `register`, `unregister`, `isRegistered`, and `onOpenUrl`
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"fs": patch
|
||||
"fs-js": patch
|
||||
---
|
||||
|
||||
Fixed `deny-webview-data` has no effect
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
nfc: patch
|
||||
nfc-js: patch
|
||||
---
|
||||
|
||||
Expect a `NFCTag` as input and return a `ScanResponse` in the `scan` function.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"updater": minor
|
||||
"updater-js": minor
|
||||
---
|
||||
|
||||
On Windows, add a new option `restartAfterInstall`/`restart_after_install` to install an update without the installer re-launching the app
|
||||
@@ -77,7 +77,9 @@
|
||||
},
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK",
|
||||
"dangerousInsecureTransportProtocol": true,
|
||||
"endpoints": [
|
||||
"http://localhost:5173/updater-test/updater.json",
|
||||
"https://tauri-update-server.vercel.app/update/{{target}}/{{current_version}}"
|
||||
]
|
||||
}
|
||||
@@ -99,6 +101,9 @@
|
||||
"localePath": "locales/pt-BR.wxl"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nsis": {
|
||||
"compression": "none"
|
||||
}
|
||||
},
|
||||
"iOS": {
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
<script>
|
||||
import { check } from '@tauri-apps/plugin-updater'
|
||||
<script lang="ts">
|
||||
import { check, Update } from '@tauri-apps/plugin-updater'
|
||||
import { relaunch } from '@tauri-apps/plugin-process'
|
||||
import { onDestroy } from 'svelte'
|
||||
|
||||
export let onMessage
|
||||
let { onMessage } = $props()
|
||||
|
||||
let isChecking, isInstalling, newUpdate
|
||||
let totalSize = 0,
|
||||
downloadedSize = 0
|
||||
let isChecking = $state(false)
|
||||
let isInstalling = $state(false)
|
||||
let newUpdate = $state<Update | undefined>()
|
||||
let totalSize = $state(0)
|
||||
let downloadedSize = $state(0)
|
||||
let progress = $derived(
|
||||
totalSize ? Math.round((downloadedSize / totalSize) * 100) : 0
|
||||
)
|
||||
|
||||
async function checkUpdate() {
|
||||
isChecking = true
|
||||
@@ -31,10 +37,10 @@
|
||||
isInstalling = true
|
||||
downloadedSize = 0
|
||||
try {
|
||||
await newUpdate.downloadAndInstall((downloadProgress) => {
|
||||
await newUpdate!.downloadAndInstall((downloadProgress) => {
|
||||
switch (downloadProgress.event) {
|
||||
case 'Started':
|
||||
totalSize = downloadProgress.data.contentLength
|
||||
totalSize = downloadProgress.data.contentLength!
|
||||
break
|
||||
case 'Progress':
|
||||
downloadedSize += downloadProgress.data.chunkLength
|
||||
@@ -54,14 +60,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: progress = totalSize ? Math.round((downloadedSize / totalSize) * 100) : 0
|
||||
onDestroy(() => {
|
||||
newUpdate?.close()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="flex children:grow children:h10">
|
||||
{#if !isChecking && !newUpdate}
|
||||
<button class="btn" on:click={checkUpdate}>Check update</button>
|
||||
<button class="btn" onclick={checkUpdate}>Check update</button>
|
||||
{:else if !isInstalling && newUpdate}
|
||||
<button class="btn" on:click={install}>Install update</button>
|
||||
<button class="btn" onclick={install}>Install update</button>
|
||||
{:else}
|
||||
<div class="progress">
|
||||
<span>{progress}%</span>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
*
|
||||
|
||||
!.gitignore
|
||||
!README.md
|
||||
!updater.json
|
||||
@@ -0,0 +1,10 @@
|
||||
To test the updater:
|
||||
|
||||
1. Comment out `verify_signature` inside `plugins/updater/src/updater.rs`
|
||||
2. Run `pnpm tauri build --debug` to build the bundles
|
||||
3. Copy the bundle you want to test to this folder (`examples/api/updater-test/`)
|
||||
4. Run `pnpm tauri dev` and open the Updater tab to check
|
||||
|
||||
If the bundle you're testing doesn't exist in the `updater.json` yet, add it manually and welcome to open an PR
|
||||
|
||||
> The `updater.json` and debug bundles will be served by `vite`
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "2.1.0",
|
||||
"notes": "Test update!",
|
||||
"pub_date": "2026-03-01T14:04:20+00:00",
|
||||
"platforms": {
|
||||
"windows-x86_64": {
|
||||
"signature": "",
|
||||
"url": "http://localhost:5173/updater-test/Tauri API_2.0.0_x64_en-US.msi"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export default defineConfig(async () => {
|
||||
port: 5173,
|
||||
strictPort: true,
|
||||
fs: {
|
||||
allow: ['.', '../../tooling/api/dist']
|
||||
allow: ['.']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,11 @@ import { type UnlistenFn, listen } from '@tauri-apps/api/event'
|
||||
* const urls = await getCurrent();
|
||||
* ```
|
||||
*
|
||||
* #### - **Windows / Linux**: This function reads the command line arguments and checks if there's only one value, which must be an URL with scheme matching one of the configured values.
|
||||
* Note that you must manually check the arguments when registering deep link schemes dynamically with [`Self::register`].
|
||||
* Additionally, the deep link might have been provided as a CLI argument so you should check if its format matches what you expect..
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Windows / Linux:** This function reads the command line arguments and checks if there's only one value, which must be an URL with scheme matching one of the configured values.
|
||||
* Note that you must manually check the arguments when registering deep link schemes dynamically with [`Self::register`].
|
||||
* Additionally, the deep link might have been provided as a CLI argument so you should check if its format matches what you expect.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@@ -35,7 +37,9 @@ export async function getCurrent(): Promise<string[] | null> {
|
||||
* await register("my-scheme");
|
||||
* ```
|
||||
*
|
||||
* #### - **macOS / Android / iOS**: Unsupported.
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **macOS / Android / iOS:** Unsupported.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@@ -54,7 +58,9 @@ export async function register(protocol: string): Promise<null> {
|
||||
* await unregister("my-scheme");
|
||||
* ```
|
||||
*
|
||||
* #### - **macOS / Linux / Android / iOS**: Unsupported.
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **macOS / Linux / Android / iOS:** Unsupported.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@@ -73,7 +79,9 @@ export async function unregister(protocol: string): Promise<null> {
|
||||
* await isRegistered("my-scheme");
|
||||
* ```
|
||||
*
|
||||
* #### - **macOS / Android / iOS**: Unsupported.
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **macOS / Android / iOS:** Unsupported.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@@ -92,7 +100,9 @@ export async function isRegistered(protocol: string): Promise<boolean> {
|
||||
* await onOpenUrl((urls) => { console.log(urls) });
|
||||
* ```
|
||||
*
|
||||
* #### - **Windows / Linux**: Unsupported without the single-instance plugin. The OS will spawn a new app instance passing the URL as a CLI argument.
|
||||
* #### Platform-specific
|
||||
*
|
||||
* - **Windows / Linux:** Unsupported without the single-instance plugin. The OS will spawn a new app instance passing the URL as a CLI argument.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,7 @@ description = """This denies read access to the
|
||||
`$APPLOCALDATA` folder on linux as the webview data and configuration values are stored here.
|
||||
Allowing access can lead to sensitive information disclosure and should be well considered."""
|
||||
|
||||
[[scope.deny]]
|
||||
[[permission.scope.deny]]
|
||||
path = "$APPLOCALDATA/**"
|
||||
|
||||
[[permission]]
|
||||
@@ -15,5 +15,5 @@ description = """This denies read access to the
|
||||
`$APPLOCALDATA/EBWebView` folder on windows as the webview data and configuration values are stored here.
|
||||
Allowing access can lead to sensitive information disclosure and should be well considered."""
|
||||
|
||||
[[scope.deny]]
|
||||
[[permission.scope.deny]]
|
||||
path = "$APPLOCALDATA/EBWebView/**"
|
||||
|
||||
+106
-109
@@ -74,7 +74,7 @@ pub type CommandResult<T> = std::result::Result<T, CommandError>;
|
||||
/// Represents either a plain PathBuf or a PathHandle that manages security-scoped resources.
|
||||
pub enum PathKind<R: Runtime> {
|
||||
/// A plain path that doesn't manage security-scoped resources.
|
||||
#[allow(dead_code)] // only used on mobile
|
||||
#[cfg(mobile)] // only used on mobile
|
||||
Path(PathBuf),
|
||||
/// A path handle that manages security-scoped resources and will clean them up on drop.
|
||||
Handle(PathHandle<R>),
|
||||
@@ -84,6 +84,7 @@ impl<R: Runtime> PathKind<R> {
|
||||
/// Get a reference to the underlying path.
|
||||
pub fn as_path(&self) -> &Path {
|
||||
match self {
|
||||
#[cfg(mobile)]
|
||||
PathKind::Path(p) => p.as_ref(),
|
||||
PathKind::Handle(h) => h.as_ref(),
|
||||
}
|
||||
@@ -92,6 +93,7 @@ impl<R: Runtime> PathKind<R> {
|
||||
/// Get a reference to the underlying PathBuf.
|
||||
pub fn as_path_buf(&self) -> &PathBuf {
|
||||
match self {
|
||||
#[cfg(mobile)]
|
||||
PathKind::Path(p) => p,
|
||||
PathKind::Handle(h) => h,
|
||||
}
|
||||
@@ -114,27 +116,13 @@ impl<R: Runtime> AsRef<PathBuf> for PathKind<R> {
|
||||
pub struct FileHandle<R: Runtime> {
|
||||
file: File,
|
||||
path: PathKind<R>,
|
||||
#[allow(dead_code)] // Used in Drop implementation
|
||||
#[cfg(target_os = "ios")]
|
||||
path_: SafeFilePath,
|
||||
#[allow(dead_code)] // Used in Drop implementation
|
||||
#[cfg(target_os = "ios")]
|
||||
app_handle: tauri::AppHandle<R>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> FileHandle<R> {
|
||||
fn new(
|
||||
file: File,
|
||||
path: PathKind<R>,
|
||||
path_: SafeFilePath,
|
||||
app_handle: tauri::AppHandle<R>,
|
||||
) -> Self {
|
||||
Self {
|
||||
file,
|
||||
path,
|
||||
path_,
|
||||
app_handle,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the resolved path.
|
||||
pub fn path(&self) -> &Path {
|
||||
self.path.as_path()
|
||||
@@ -155,41 +143,39 @@ impl<R: Runtime> DerefMut for FileHandle<R> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
impl<R: Runtime> Drop for FileHandle<R> {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
// Only clean up if we have a plain PathBuf, not a PathHandle
|
||||
// PathHandle will handle its own cleanup when it's dropped
|
||||
if let PathKind::Path(_) = &self.path {
|
||||
use crate::{FilePath, FsExt};
|
||||
// Convert SafeFilePath to FilePath
|
||||
let file_path: FilePath = match &self.path_ {
|
||||
SafeFilePath::Url(url) => FilePath::Url(url.clone()),
|
||||
SafeFilePath::Path(safe_path) => FilePath::Path(safe_path.as_ref().to_owned()),
|
||||
};
|
||||
// Only clean up if we have a plain PathBuf, not a PathHandle
|
||||
// PathHandle will handle its own cleanup when it's dropped
|
||||
if let PathKind::Path(_) = &self.path {
|
||||
use crate::{FilePath, FsExt};
|
||||
// Convert SafeFilePath to FilePath
|
||||
let file_path: FilePath = match &self.path_ {
|
||||
SafeFilePath::Url(url) => FilePath::Url(url.clone()),
|
||||
SafeFilePath::Path(safe_path) => FilePath::Path(safe_path.as_ref().to_owned()),
|
||||
};
|
||||
|
||||
// Only clean up if we're tracking this resource
|
||||
// If start_accessing_security_scoped_resource was used, it won't be in our tracking
|
||||
// and we shouldn't interfere
|
||||
if let FilePath::Url(url) = file_path {
|
||||
if url.scheme() == "file" {
|
||||
let security_scoped_resources =
|
||||
self.app_handle.state::<crate::SecurityScopedResources>();
|
||||
// Only clean up if we're tracking this resource
|
||||
// If start_accessing_security_scoped_resource was used, it won't be in our tracking
|
||||
// and we shouldn't interfere
|
||||
if let FilePath::Url(url) = file_path {
|
||||
if url.scheme() == "file" {
|
||||
let security_scoped_resources =
|
||||
self.app_handle.state::<crate::SecurityScopedResources>();
|
||||
|
||||
// Only clean up if it's not tracked manually
|
||||
if !security_scoped_resources.is_tracked_manually(url.as_str()) {
|
||||
log::debug!("Stopping accessing security-scoped resource for URL: {url} on drop");
|
||||
let _ = self
|
||||
.app_handle
|
||||
.fs()
|
||||
.stop_accessing_security_scoped_resource(FilePath::Url(
|
||||
url.clone(),
|
||||
));
|
||||
security_scoped_resources.remove(url.as_str());
|
||||
} else {
|
||||
log::debug!("Not cleaning up security-scoped resource for URL: {url} on drop (manually tracked via start_accessing_security_scoped_resource)");
|
||||
}
|
||||
// Only clean up if it's not tracked manually
|
||||
if !security_scoped_resources.is_tracked_manually(url.as_str()) {
|
||||
log::debug!(
|
||||
"Stopping accessing security-scoped resource for URL: {url} on drop"
|
||||
);
|
||||
let _ = self
|
||||
.app_handle
|
||||
.fs()
|
||||
.stop_accessing_security_scoped_resource(FilePath::Url(url.clone()));
|
||||
security_scoped_resources.remove(url.as_str());
|
||||
} else {
|
||||
log::debug!("Not cleaning up security-scoped resource for URL: {url} on drop (manually tracked via start_accessing_security_scoped_resource)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,38 +222,36 @@ impl<R: Runtime> AsRef<PathBuf> for PathHandle<R> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
impl<R: Runtime> Drop for PathHandle<R> {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
use crate::{FilePath, FsExt};
|
||||
// Convert SafeFilePath to FilePath
|
||||
let file_path: FilePath = match &self.path_ {
|
||||
SafeFilePath::Url(url) => FilePath::Url(url.clone()),
|
||||
SafeFilePath::Path(safe_path) => FilePath::Path(safe_path.as_ref().to_owned()),
|
||||
};
|
||||
use crate::{FilePath, FsExt};
|
||||
// Convert SafeFilePath to FilePath
|
||||
let file_path: FilePath = match &self.path_ {
|
||||
SafeFilePath::Url(url) => FilePath::Url(url.clone()),
|
||||
SafeFilePath::Path(safe_path) => FilePath::Path(safe_path.as_ref().to_owned()),
|
||||
};
|
||||
|
||||
// Only clean up if we're tracking this resource (i.e., resolve_path started it)
|
||||
// If start_accessing_security_scoped_resource was used, it won't be in our tracking
|
||||
// and we shouldn't interfere
|
||||
if let FilePath::Url(url) = file_path {
|
||||
if url.scheme() == "file" {
|
||||
let security_scoped_resources =
|
||||
self.app_handle.state::<crate::SecurityScopedResources>();
|
||||
// Only clean up if we're tracking this resource (i.e., resolve_path started it)
|
||||
// If start_accessing_security_scoped_resource was used, it won't be in our tracking
|
||||
// and we shouldn't interfere
|
||||
if let FilePath::Url(url) = file_path {
|
||||
if url.scheme() == "file" {
|
||||
let security_scoped_resources =
|
||||
self.app_handle.state::<crate::SecurityScopedResources>();
|
||||
|
||||
// Only clean up if it's not tracked manually
|
||||
if !security_scoped_resources.is_tracked_manually(url.as_str()) {
|
||||
log::debug!(
|
||||
"Stopping accessing security-scoped resource for URL: {url} on drop"
|
||||
);
|
||||
let _ = self
|
||||
.app_handle
|
||||
.fs()
|
||||
.stop_accessing_security_scoped_resource(FilePath::Url(url.clone()));
|
||||
security_scoped_resources.remove(url.as_str());
|
||||
} else {
|
||||
log::debug!("Not cleaning up security-scoped resource for URL: {url} on drop (manually tracked via start_accessing_security_scoped_resource)");
|
||||
}
|
||||
// Only clean up if it's not tracked manually
|
||||
if !security_scoped_resources.is_tracked_manually(url.as_str()) {
|
||||
log::debug!(
|
||||
"Stopping accessing security-scoped resource for URL: {url} on drop"
|
||||
);
|
||||
let _ = self
|
||||
.app_handle
|
||||
.fs()
|
||||
.stop_accessing_security_scoped_resource(FilePath::Url(url.clone()));
|
||||
security_scoped_resources.remove(url.as_str());
|
||||
} else {
|
||||
log::debug!("Not cleaning up security-scoped resource for URL: {url} on drop (manually tracked via start_accessing_security_scoped_resource)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,6 +272,7 @@ pub fn create<R: Runtime>(
|
||||
path: SafeFilePath,
|
||||
options: Option<BaseOptions>,
|
||||
) -> CommandResult<ResourceId> {
|
||||
#[cfg(target_os = "ios")]
|
||||
let path_ = path.clone();
|
||||
let resolved_path_handle = resolve_path(
|
||||
"create",
|
||||
@@ -303,13 +288,16 @@ pub fn create<R: Runtime>(
|
||||
resolved_path_handle.display()
|
||||
)
|
||||
})?;
|
||||
#[cfg(target_os = "ios")]
|
||||
let app_handle = webview.app_handle().clone();
|
||||
let file_handle = FileHandle::new(
|
||||
let file_handle = FileHandle {
|
||||
file,
|
||||
PathKind::Handle(resolved_path_handle),
|
||||
path: PathKind::Handle(resolved_path_handle),
|
||||
#[cfg(target_os = "ios")]
|
||||
path_,
|
||||
#[cfg(target_os = "ios")]
|
||||
app_handle,
|
||||
);
|
||||
};
|
||||
let rid = webview
|
||||
.resources_table()
|
||||
.add(StdFileResource::new(file_handle));
|
||||
@@ -1417,6 +1405,7 @@ fn resolve_file_in_fs<R: Runtime>(
|
||||
path: SafeFilePath,
|
||||
open_options: OpenOptions,
|
||||
) -> CommandResult<FileHandle<R>> {
|
||||
#[cfg(target_os = "ios")]
|
||||
let path_ = path.clone();
|
||||
let resolved_path_handle = resolve_path(
|
||||
permission,
|
||||
@@ -1436,13 +1425,16 @@ fn resolve_file_in_fs<R: Runtime>(
|
||||
)
|
||||
})?;
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
let app_handle = webview.app_handle().clone();
|
||||
Ok(FileHandle::new(
|
||||
Ok(FileHandle {
|
||||
file,
|
||||
PathKind::Handle(resolved_path_handle),
|
||||
path: PathKind::Handle(resolved_path_handle),
|
||||
#[cfg(target_os = "ios")]
|
||||
path_,
|
||||
#[cfg(target_os = "ios")]
|
||||
app_handle,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(mobile)]
|
||||
@@ -1456,6 +1448,7 @@ pub fn resolve_file<R: Runtime>(
|
||||
) -> CommandResult<FileHandle<R>> {
|
||||
use crate::FsExt;
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
let path_ = path.clone();
|
||||
match path {
|
||||
SafeFilePath::Url(url) => {
|
||||
@@ -1463,13 +1456,16 @@ pub fn resolve_file<R: Runtime>(
|
||||
let file = webview
|
||||
.fs()
|
||||
.open(SafeFilePath::Url(url.clone()), open_options.options)?;
|
||||
#[cfg(target_os = "ios")]
|
||||
let app_handle = webview.app_handle().clone();
|
||||
Ok(FileHandle::new(
|
||||
Ok(FileHandle {
|
||||
file,
|
||||
PathKind::Path(resolved_path),
|
||||
path: PathKind::Path(resolved_path),
|
||||
#[cfg(target_os = "ios")]
|
||||
path_,
|
||||
#[cfg(target_os = "ios")]
|
||||
app_handle,
|
||||
))
|
||||
})
|
||||
}
|
||||
SafeFilePath::Path(path) => resolve_file_in_fs(
|
||||
permission,
|
||||
@@ -1494,36 +1490,37 @@ pub fn resolve_path<R: Runtime>(
|
||||
// On iOS, start accessing security-scoped resource if the path is a file URL
|
||||
// Only if it hasn't been started already via start_accessing_security_scoped_resource
|
||||
#[cfg(target_os = "ios")]
|
||||
{
|
||||
if let SafeFilePath::Url(url) = &path {
|
||||
if url.scheme() == "file" {
|
||||
use objc2_foundation::{NSString, NSURL};
|
||||
if let SafeFilePath::Url(url) = &path {
|
||||
if url.scheme() == "file" {
|
||||
use objc2_foundation::{NSString, NSURL};
|
||||
|
||||
let security_scoped_resources = webview.state::<crate::SecurityScopedResources>();
|
||||
let security_scoped_resources = webview.state::<crate::SecurityScopedResources>();
|
||||
|
||||
// Check if already active (started via start_accessing_security_scoped_resource)
|
||||
if !security_scoped_resources.is_tracked_manually(url.as_str()) {
|
||||
let url_nsstring = NSString::from_str(url.as_str());
|
||||
let ns_url = unsafe { NSURL::URLWithString(&url_nsstring) };
|
||||
if let Some(ns_url) = ns_url {
|
||||
// Start accessing the security-scoped resource
|
||||
// This is required for files outside the app's sandbox (e.g., from file picker)
|
||||
unsafe {
|
||||
let success = ns_url.startAccessingSecurityScopedResource();
|
||||
if success {
|
||||
log::debug!("Started accessing security-scoped resource for URL: {} (via resolve_path)", url.as_str());
|
||||
// Track it so we know to clean it up
|
||||
security_scoped_resources.track_manually(url.as_str().to_string());
|
||||
} else {
|
||||
log::warn!("Failed to start accessing security-scoped resource for URL: {}", url.as_str());
|
||||
}
|
||||
// Check if already active (started via start_accessing_security_scoped_resource)
|
||||
if !security_scoped_resources.is_tracked_manually(url.as_str()) {
|
||||
let url_nsstring = NSString::from_str(url.as_str());
|
||||
let ns_url = unsafe { NSURL::URLWithString(&url_nsstring) };
|
||||
if let Some(ns_url) = ns_url {
|
||||
// Start accessing the security-scoped resource
|
||||
// This is required for files outside the app's sandbox (e.g., from file picker)
|
||||
unsafe {
|
||||
let success = ns_url.startAccessingSecurityScopedResource();
|
||||
if success {
|
||||
log::debug!("Started accessing security-scoped resource for URL: {} (via resolve_path)", url.as_str());
|
||||
// Track it so we know to clean it up
|
||||
security_scoped_resources.track_manually(url.as_str().to_string());
|
||||
} else {
|
||||
log::warn!(
|
||||
"Failed to start accessing security-scoped resource for URL: {}",
|
||||
url.as_str()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log::debug!("Failed to create NSURL from URL: {}, ignoring security-scoped resource access request", url.as_str());
|
||||
}
|
||||
} else {
|
||||
log::debug!("Security-scoped resource already active for URL: {} (started via start_accessing_security_scoped_resource), skipping", url.as_str());
|
||||
log::debug!("Failed to create NSURL from URL: {}, ignoring security-scoped resource access request", url.as_str());
|
||||
}
|
||||
} else {
|
||||
log::debug!("Security-scoped resource already active for URL: {} (started via start_accessing_security_scoped_resource), skipping", url.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ impl<R: Runtime> Nfc<R> {
|
||||
pub fn scan(&self, payload: ScanRequest) -> crate::Result<ScanResponse> {
|
||||
self.0
|
||||
.run_mobile_plugin("scan", payload)
|
||||
.map(|v| ScanResponse { tag: v })
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
if("__TAURI__"in window){var __TAURI_PLUGIN_UPDATER__=function(t){"use strict";function e(t,e,s,n){if("function"==typeof e?t!==e||!n:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===s?n:"a"===s?n.call(t):n?n.value:e.get(t)}function s(t,e,s,n,i){if("function"==typeof e||!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(t,s),s}var n,i,a,r,o;"function"==typeof SuppressedError&&SuppressedError;const d="__TAURI_TO_IPC_KEY__";class c{constructor(t){n.set(this,void 0),i.set(this,0),a.set(this,[]),r.set(this,void 0),s(this,n,t||(()=>{})),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}(t=>{const o=t.index;if("end"in t)return void(o==e(this,i,"f")?this.cleanupCallback():s(this,r,o));const d=t.message;if(o==e(this,i,"f")){for(e(this,n,"f").call(this,d),s(this,i,e(this,i,"f")+1);e(this,i,"f")in e(this,a,"f");){const t=e(this,a,"f")[e(this,i,"f")];e(this,n,"f").call(this,t),delete e(this,a,"f")[e(this,i,"f")],s(this,i,e(this,i,"f")+1)}e(this,i,"f")===e(this,r,"f")&&this.cleanupCallback()}else e(this,a,"f")[o]=d})}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(t){s(this,n,t)}get onmessage(){return e(this,n,"f")}[(n=new WeakMap,i=new WeakMap,a=new WeakMap,r=new WeakMap,d)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[d]()}}async function l(t,e={},s){return window.__TAURI_INTERNALS__.invoke(t,e,s)}class h{get rid(){return e(this,o,"f")}constructor(t){o.set(this,void 0),s(this,o,t)}async close(){return l("plugin:resources|close",{rid:this.rid})}}o=new WeakMap;class u extends h{constructor(t){super(t.rid),this.available=!0,this.currentVersion=t.currentVersion,this.version=t.version,this.date=t.date,this.body=t.body,this.rawJson=t.rawJson}async download(t,e){_(e);const s=new c;t&&(s.onmessage=t);const n=await l("plugin:updater|download",{onEvent:s,rid:this.rid,...e});this.downloadedBytes=new h(n)}async install(){if(!this.downloadedBytes)throw new Error("Update.install called before Update.download");await l("plugin:updater|install",{updateRid:this.rid,bytesRid:this.downloadedBytes.rid}),this.downloadedBytes=void 0}async downloadAndInstall(t,e){_(e);const s=new c;t&&(s.onmessage=t),await l("plugin:updater|download_and_install",{onEvent:s,rid:this.rid,...e})}async close(){await(this.downloadedBytes?.close()),await super.close()}}function _(t){t?.headers&&(t.headers=Array.from(new Headers(t.headers).entries()))}return t.Update=u,t.check=async function(t){_(t);const e=await l("plugin:updater|check",{...t});return e?new u(e):null},t}({});Object.defineProperty(window.__TAURI__,"updater",{value:__TAURI_PLUGIN_UPDATER__})}
|
||||
if("__TAURI__"in window){var __TAURI_PLUGIN_UPDATER__=function(t){"use strict";function e(t,e,s,n){if("function"==typeof e?t!==e||!n:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===s?n:"a"===s?n.call(t):n?n.value:e.get(t)}function s(t,e,s,n,i){if("function"==typeof e||!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(t,s),s}var n,i,a,r,o;"function"==typeof SuppressedError&&SuppressedError;const d="__TAURI_TO_IPC_KEY__";class c{constructor(t){n.set(this,void 0),i.set(this,0),a.set(this,[]),r.set(this,void 0),s(this,n,t||(()=>{})),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}(t=>{const o=t.index;if("end"in t)return void(o==e(this,i,"f")?this.cleanupCallback():s(this,r,o));const d=t.message;if(o==e(this,i,"f")){for(e(this,n,"f").call(this,d),s(this,i,e(this,i,"f")+1);e(this,i,"f")in e(this,a,"f");){const t=e(this,a,"f")[e(this,i,"f")];e(this,n,"f").call(this,t),delete e(this,a,"f")[e(this,i,"f")],s(this,i,e(this,i,"f")+1)}e(this,i,"f")===e(this,r,"f")&&this.cleanupCallback()}else e(this,a,"f")[o]=d})}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(t){s(this,n,t)}get onmessage(){return e(this,n,"f")}[(n=new WeakMap,i=new WeakMap,a=new WeakMap,r=new WeakMap,d)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[d]()}}async function l(t,e={},s){return window.__TAURI_INTERNALS__.invoke(t,e,s)}class h{get rid(){return e(this,o,"f")}constructor(t){o.set(this,void 0),s(this,o,t)}async close(){return l("plugin:resources|close",{rid:this.rid})}}o=new WeakMap;class u extends h{constructor(t){super(t.rid),this.available=!0,this.currentVersion=t.currentVersion,this.version=t.version,this.date=t.date,this.body=t.body,this.rawJson=t.rawJson}async download(t,e){_(e);const s=new c;t&&(s.onmessage=t);const n=await l("plugin:updater|download",{onEvent:s,rid:this.rid,...e});this.downloadedBytes=new h(n)}async install(t){if(!this.downloadedBytes)throw new Error("Update.install called before Update.download");await l("plugin:updater|install",{updateRid:this.rid,bytesRid:this.downloadedBytes.rid,...t}),this.downloadedBytes=void 0}async downloadAndInstall(t,e){_(e);const s=new c;t&&(s.onmessage=t),await l("plugin:updater|download_and_install",{onEvent:s,rid:this.rid,...e})}async close(){await(this.downloadedBytes?.close()),await super.close()}}function _(t){t?.headers&&(t.headers=Array.from(new Headers(t.headers).entries()))}return t.Update=u,t.check=async function(t){_(t);const e=await l("plugin:updater|check",{...t});return e?new u(e):null},t}({});Object.defineProperty(window.__TAURI__,"updater",{value:__TAURI_PLUGIN_UPDATER__})}
|
||||
|
||||
@@ -40,6 +40,14 @@ interface DownloadOptions {
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
/** Options used when installing an update */
|
||||
interface InstallOptions {
|
||||
/**
|
||||
* If the Windows installer should restart the app after installed, default is `true`
|
||||
*/
|
||||
restartAfterInstall?: boolean
|
||||
}
|
||||
|
||||
interface UpdateMetadata {
|
||||
rid: number
|
||||
currentVersion: string
|
||||
@@ -95,14 +103,15 @@ class Update extends Resource {
|
||||
}
|
||||
|
||||
/** Install downloaded updater package */
|
||||
async install(): Promise<void> {
|
||||
async install(options?: InstallOptions): Promise<void> {
|
||||
if (!this.downloadedBytes) {
|
||||
throw new Error('Update.install called before Update.download')
|
||||
}
|
||||
|
||||
await invoke('plugin:updater|install', {
|
||||
updateRid: this.rid,
|
||||
bytesRid: this.downloadedBytes.rid
|
||||
bytesRid: this.downloadedBytes.rid,
|
||||
...options
|
||||
})
|
||||
|
||||
// Don't need to call close, we did it in rust side already
|
||||
@@ -112,7 +121,7 @@ class Update extends Resource {
|
||||
/** Downloads the updater package and installs it */
|
||||
async downloadAndInstall(
|
||||
onEvent?: (progress: DownloadEvent) => void,
|
||||
options?: DownloadOptions
|
||||
options?: DownloadOptions & InstallOptions
|
||||
): Promise<void> {
|
||||
convertToRustHeaders(options)
|
||||
const channel = new Channel<DownloadEvent>()
|
||||
|
||||
@@ -142,12 +142,22 @@ pub(crate) async fn install<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
update_rid: ResourceId,
|
||||
bytes_rid: ResourceId,
|
||||
restart_after_install: Option<bool>,
|
||||
) -> Result<()> {
|
||||
let update = webview.resources_table().get::<Update>(update_rid)?;
|
||||
let bytes = webview
|
||||
.resources_table()
|
||||
.get::<DownloadedBytes>(bytes_rid)?;
|
||||
update.install(&bytes.0)?;
|
||||
|
||||
if let Some(restart_after_install) = restart_after_install {
|
||||
let update = (*update).clone();
|
||||
update
|
||||
.restart_after_install(restart_after_install)
|
||||
.install(&bytes.0)?;
|
||||
} else {
|
||||
update.install(&bytes.0)?;
|
||||
}
|
||||
|
||||
let _ = webview.resources_table().close(bytes_rid);
|
||||
Ok(())
|
||||
}
|
||||
@@ -159,6 +169,7 @@ pub(crate) async fn download_and_install<R: Runtime>(
|
||||
on_event: Channel<DownloadEvent>,
|
||||
headers: Option<Vec<(String, String)>>,
|
||||
timeout: Option<u64>,
|
||||
restart_after_install: Option<bool>,
|
||||
) -> Result<()> {
|
||||
let update = webview.resources_table().get::<Update>(rid)?;
|
||||
|
||||
@@ -176,6 +187,10 @@ pub(crate) async fn download_and_install<R: Runtime>(
|
||||
update.timeout = Some(Duration::from_millis(timeout));
|
||||
}
|
||||
|
||||
if let Some(restart_after_install) = restart_after_install {
|
||||
update = update.restart_after_install(restart_after_install);
|
||||
}
|
||||
|
||||
let mut first_chunk = true;
|
||||
|
||||
update
|
||||
|
||||
@@ -32,17 +32,30 @@ impl WindowsUpdateInstallMode {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn msi_restart_after_install_args(&self) -> &'static [&'static str] {
|
||||
&["AUTOLAUNCHAPP=True"]
|
||||
}
|
||||
|
||||
/// Returns the associated nsis arguments.
|
||||
pub fn nsis_args(&self) -> &'static [&'static str] {
|
||||
// `/P`: Passive
|
||||
// `/S`: Silent
|
||||
// `/R`: Restart
|
||||
match self {
|
||||
Self::Passive => &["/P", "/R"],
|
||||
Self::Quiet => &["/S", "/R"],
|
||||
Self::Passive => &["/P"],
|
||||
Self::Quiet => &["/S"],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn nsis_restart_after_install_args(&self) -> &'static [&'static str] {
|
||||
match self {
|
||||
Self::BasicUi => &[],
|
||||
_ => &["/R"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for WindowsUpdateInstallMode {
|
||||
@@ -63,6 +76,8 @@ impl Display for WindowsUpdateInstallMode {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WindowsConfig {
|
||||
/// Additional arguments given to the NSIS or WiX installer.
|
||||
///
|
||||
/// Note: this applies to both WiX and NSIS installers
|
||||
#[serde(
|
||||
default,
|
||||
alias = "installer-args",
|
||||
|
||||
@@ -83,9 +83,9 @@ impl<R: Runtime, T: Manager<R>> UpdaterExt<R> for T {
|
||||
builder = builder.target(target);
|
||||
}
|
||||
|
||||
let args = self.env().args_os;
|
||||
if !args.is_empty() {
|
||||
builder = builder.current_exe_args(args);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
builder = builder.current_exe_args(self.env().args_os);
|
||||
}
|
||||
|
||||
builder.version_comparator = version_comparator.clone();
|
||||
|
||||
+212
-148
@@ -129,16 +129,34 @@ impl RemoteRelease {
|
||||
pub type OnBeforeExit = Arc<dyn Fn() + Send + Sync + 'static>;
|
||||
pub type OnBeforeRequest = Arc<dyn Fn(ClientBuilder) -> ClientBuilder + Send + Sync + 'static>;
|
||||
pub type VersionComparator = Arc<dyn Fn(Version, RemoteRelease) -> bool + Send + Sync>;
|
||||
#[cfg(target_os = "macos")]
|
||||
type MainThreadClosure = Box<dyn FnOnce() + Send + Sync + 'static>;
|
||||
type RunOnMainThread =
|
||||
Box<dyn Fn(MainThreadClosure) -> std::result::Result<(), tauri::Error> + Send + Sync + 'static>;
|
||||
#[cfg(target_os = "macos")]
|
||||
type RunOnMainThread = Arc<dyn Fn(MainThreadClosure) -> tauri::Result<()> + Send + Sync + 'static>;
|
||||
|
||||
// TODO: Move more fields to this in v3 if we can mark those fields non `pub`
|
||||
/// Updater context shared between [`UpdaterBuilder`], [`Updater`] and [`Update`]
|
||||
#[derive(Clone)]
|
||||
struct UpdaterContext {
|
||||
config: Config,
|
||||
configure_client: Option<OnBeforeRequest>,
|
||||
#[cfg(target_os = "macos")]
|
||||
run_on_main_thread: RunOnMainThread,
|
||||
/// App name, used for creating named tempfiles
|
||||
#[cfg(windows)]
|
||||
app_name: String,
|
||||
#[cfg(windows)]
|
||||
installer_args: Vec<OsString>,
|
||||
#[cfg(windows)]
|
||||
current_exe_args: Vec<OsString>,
|
||||
#[cfg(windows)]
|
||||
on_before_exit: Option<OnBeforeExit>,
|
||||
#[cfg(windows)]
|
||||
restart_after_install: bool,
|
||||
}
|
||||
|
||||
pub struct UpdaterBuilder {
|
||||
#[allow(dead_code)]
|
||||
run_on_main_thread: RunOnMainThread,
|
||||
app_name: String,
|
||||
current_version: Version,
|
||||
config: Config,
|
||||
pub(crate) version_comparator: Option<VersionComparator>,
|
||||
executable_path: Option<PathBuf>,
|
||||
target: Option<String>,
|
||||
@@ -147,27 +165,38 @@ pub struct UpdaterBuilder {
|
||||
timeout: Option<Duration>,
|
||||
proxy: Option<Url>,
|
||||
no_proxy: bool,
|
||||
installer_args: Vec<OsString>,
|
||||
current_exe_args: Vec<OsString>,
|
||||
on_before_exit: Option<OnBeforeExit>,
|
||||
configure_client: Option<OnBeforeRequest>,
|
||||
context: UpdaterContext,
|
||||
}
|
||||
|
||||
impl UpdaterBuilder {
|
||||
pub(crate) fn new<R: Runtime>(app: &AppHandle<R>, config: crate::Config) -> Self {
|
||||
let app_ = app.clone();
|
||||
let run_on_main_thread = move |f| app_.run_on_main_thread(f);
|
||||
#[cfg(target_os = "macos")]
|
||||
let run_on_main_thread = {
|
||||
let app_ = app.clone();
|
||||
Arc::new(move |f| app_.run_on_main_thread(f))
|
||||
};
|
||||
Self {
|
||||
run_on_main_thread: Box::new(run_on_main_thread),
|
||||
installer_args: config
|
||||
.windows
|
||||
.as_ref()
|
||||
.map(|w| w.installer_args.clone())
|
||||
.unwrap_or_default(),
|
||||
current_exe_args: Vec::new(),
|
||||
app_name: app.package_info().name.clone(),
|
||||
context: UpdaterContext {
|
||||
#[cfg(windows)]
|
||||
installer_args: config
|
||||
.windows
|
||||
.as_ref()
|
||||
.map(|w| w.installer_args.clone())
|
||||
.unwrap_or_default(),
|
||||
config,
|
||||
configure_client: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
run_on_main_thread,
|
||||
#[cfg(windows)]
|
||||
app_name: app.package_info().name.clone(),
|
||||
#[cfg(windows)]
|
||||
current_exe_args: Vec::new(),
|
||||
#[cfg(windows)]
|
||||
on_before_exit: None,
|
||||
#[cfg(windows)]
|
||||
restart_after_install: true,
|
||||
},
|
||||
current_version: app.package_info().version.clone(),
|
||||
config,
|
||||
version_comparator: None,
|
||||
executable_path: None,
|
||||
target: None,
|
||||
@@ -176,8 +205,6 @@ impl UpdaterBuilder {
|
||||
timeout: None,
|
||||
proxy: None,
|
||||
no_proxy: false,
|
||||
on_before_exit: None,
|
||||
configure_client: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +224,7 @@ impl UpdaterBuilder {
|
||||
pub fn endpoints(mut self, endpoints: Vec<Url>) -> Result<Self> {
|
||||
crate::config::validate_endpoints(
|
||||
&endpoints,
|
||||
self.config.dangerous_insecure_transport_protocol,
|
||||
self.context.config.dangerous_insecure_transport_protocol,
|
||||
)?;
|
||||
|
||||
self.endpoints.replace(endpoints);
|
||||
@@ -251,26 +278,40 @@ impl UpdaterBuilder {
|
||||
}
|
||||
|
||||
pub fn pubkey<S: Into<String>>(mut self, pubkey: S) -> Self {
|
||||
self.config.pubkey = pubkey.into();
|
||||
self.context.config.pubkey = pubkey.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an argument to pass to the Windows installer.
|
||||
///
|
||||
/// Note: this applies to both WiX and NSIS installers
|
||||
#[cfg_attr(not(windows), allow(unused))]
|
||||
pub fn installer_arg<S>(mut self, arg: S) -> Self
|
||||
where
|
||||
S: Into<OsString>,
|
||||
{
|
||||
self.installer_args.push(arg.into());
|
||||
#[cfg(windows)]
|
||||
{
|
||||
self.context.installer_args.push(arg.into());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds multiple arguments to pass to the Windows installer.
|
||||
///
|
||||
/// Note: this applies to both WiX and NSIS installers
|
||||
#[cfg_attr(not(windows), allow(unused))]
|
||||
pub fn installer_args<I, S>(mut self, args: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<OsString>,
|
||||
{
|
||||
self.installer_args.extend(args.into_iter().map(Into::into));
|
||||
#[cfg(windows)]
|
||||
{
|
||||
self.context
|
||||
.installer_args
|
||||
.extend(args.into_iter().map(Into::into));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
@@ -280,14 +321,32 @@ impl UpdaterBuilder {
|
||||
/// [`Self::installer_arg`], [`crate::Builder::installer_arg`]
|
||||
/// and the `plugins > updater > windows > installerArgs` config,
|
||||
/// not the ones managed by us (e.g. `/UPDATER` flag passed to the NSIS installer)
|
||||
#[cfg_attr(not(windows), allow(unused))]
|
||||
pub fn clear_installer_args(mut self) -> Self {
|
||||
self.installer_args.clear();
|
||||
#[cfg(windows)]
|
||||
{
|
||||
self.context.installer_args.clear();
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Function to run before we run the installer and exit the app through `std::process::exit(0)` on Windows
|
||||
#[cfg_attr(not(windows), allow(unused))]
|
||||
pub fn on_before_exit<F: Fn() + Send + Sync + 'static>(mut self, f: F) -> Self {
|
||||
self.on_before_exit.replace(Arc::new(f));
|
||||
#[cfg(windows)]
|
||||
{
|
||||
self.context.on_before_exit.replace(Arc::new(f));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// If the Windows installer should restart the app after installed, default is `true`
|
||||
#[cfg_attr(not(windows), allow(unused))]
|
||||
pub fn restart_after_install(mut self, restart_after_install: bool) -> Self {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
self.context.restart_after_install = restart_after_install;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
@@ -299,14 +358,14 @@ impl UpdaterBuilder {
|
||||
mut self,
|
||||
f: F,
|
||||
) -> Self {
|
||||
self.configure_client.replace(Arc::new(f));
|
||||
self.context.configure_client.replace(Arc::new(f));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<Updater> {
|
||||
let endpoints = self
|
||||
.endpoints
|
||||
.unwrap_or_else(|| self.config.endpoints.clone());
|
||||
.unwrap_or_else(|| self.context.config.endpoints.clone());
|
||||
|
||||
if endpoints.is_empty() {
|
||||
return Err(Error::EmptyEndpoints);
|
||||
@@ -324,44 +383,36 @@ impl UpdaterBuilder {
|
||||
};
|
||||
|
||||
Ok(Updater {
|
||||
run_on_main_thread: Arc::new(self.run_on_main_thread),
|
||||
config: self.config,
|
||||
app_name: self.app_name,
|
||||
current_version: self.current_version,
|
||||
version_comparator: self.version_comparator,
|
||||
timeout: self.timeout,
|
||||
proxy: self.proxy,
|
||||
no_proxy: self.no_proxy,
|
||||
endpoints,
|
||||
installer_args: self.installer_args,
|
||||
current_exe_args: self.current_exe_args,
|
||||
arch,
|
||||
target: self.target,
|
||||
headers: self.headers,
|
||||
extract_path,
|
||||
on_before_exit: self.on_before_exit,
|
||||
configure_client: self.configure_client,
|
||||
context: self.context.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
impl UpdaterBuilder {
|
||||
pub(crate) fn current_exe_args<I, S>(mut self, args: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<OsString>,
|
||||
{
|
||||
self.current_exe_args
|
||||
self.context
|
||||
.current_exe_args
|
||||
.extend(args.into_iter().map(Into::into));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Updater {
|
||||
#[allow(dead_code)]
|
||||
run_on_main_thread: Arc<RunOnMainThread>,
|
||||
config: Config,
|
||||
app_name: String,
|
||||
current_version: Version,
|
||||
version_comparator: Option<VersionComparator>,
|
||||
timeout: Option<Duration>,
|
||||
@@ -374,12 +425,7 @@ pub struct Updater {
|
||||
target: Option<String>,
|
||||
headers: HeaderMap,
|
||||
extract_path: PathBuf,
|
||||
on_before_exit: Option<OnBeforeExit>,
|
||||
configure_client: Option<OnBeforeRequest>,
|
||||
#[allow(unused)]
|
||||
installer_args: Vec<OsString>,
|
||||
#[allow(unused)]
|
||||
current_exe_args: Vec<OsString>,
|
||||
context: UpdaterContext,
|
||||
}
|
||||
|
||||
impl Updater {
|
||||
@@ -449,10 +495,10 @@ impl Updater {
|
||||
}
|
||||
|
||||
let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT);
|
||||
if self.config.dangerous_accept_invalid_certs {
|
||||
if self.context.config.dangerous_accept_invalid_certs {
|
||||
request = request.danger_accept_invalid_certs(true);
|
||||
}
|
||||
if self.config.dangerous_accept_invalid_hostnames {
|
||||
if self.context.config.dangerous_accept_invalid_hostnames {
|
||||
request = request.danger_accept_invalid_hostnames(true);
|
||||
}
|
||||
if let Some(timeout) = self.timeout {
|
||||
@@ -467,7 +513,7 @@ impl Updater {
|
||||
request = request.proxy(proxy);
|
||||
}
|
||||
|
||||
if let Some(ref configure_client) = self.configure_client {
|
||||
if let Some(ref configure_client) = self.context.configure_client {
|
||||
request = configure_client(request);
|
||||
}
|
||||
|
||||
@@ -537,10 +583,6 @@ impl Updater {
|
||||
|
||||
let update = if should_update {
|
||||
Some(Update {
|
||||
run_on_main_thread: self.run_on_main_thread.clone(),
|
||||
config: self.config.clone(),
|
||||
on_before_exit: self.on_before_exit.clone(),
|
||||
app_name: self.app_name.clone(),
|
||||
current_version: self.current_version.to_string(),
|
||||
target: target.to_owned(),
|
||||
extract_path: self.extract_path.clone(),
|
||||
@@ -554,9 +596,7 @@ impl Updater {
|
||||
proxy: self.proxy.clone(),
|
||||
no_proxy: self.no_proxy,
|
||||
headers: self.headers.clone(),
|
||||
installer_args: self.installer_args.clone(),
|
||||
current_exe_args: self.current_exe_args.clone(),
|
||||
configure_client: self.configure_client.clone(),
|
||||
context: self.context.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -600,11 +640,6 @@ impl Updater {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Update {
|
||||
#[allow(dead_code)]
|
||||
run_on_main_thread: Arc<RunOnMainThread>,
|
||||
config: Config,
|
||||
#[allow(unused)]
|
||||
on_before_exit: Option<OnBeforeExit>,
|
||||
/// Update description
|
||||
pub body: Option<String>,
|
||||
/// Version used to check for update
|
||||
@@ -633,14 +668,7 @@ pub struct Update {
|
||||
/// Extract path
|
||||
#[allow(unused)]
|
||||
extract_path: PathBuf,
|
||||
/// App name, used for creating named tempfiles on Windows
|
||||
#[allow(unused)]
|
||||
app_name: String,
|
||||
#[allow(unused)]
|
||||
installer_args: Vec<OsString>,
|
||||
#[allow(unused)]
|
||||
current_exe_args: Vec<OsString>,
|
||||
configure_client: Option<OnBeforeRequest>,
|
||||
context: UpdaterContext,
|
||||
}
|
||||
|
||||
impl Resource for Update {}
|
||||
@@ -661,10 +689,10 @@ impl Update {
|
||||
}
|
||||
|
||||
let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT);
|
||||
if self.config.dangerous_accept_invalid_certs {
|
||||
if self.context.config.dangerous_accept_invalid_certs {
|
||||
request = request.danger_accept_invalid_certs(true);
|
||||
}
|
||||
if self.config.dangerous_accept_invalid_hostnames {
|
||||
if self.context.config.dangerous_accept_invalid_hostnames {
|
||||
request = request.danger_accept_invalid_hostnames(true);
|
||||
}
|
||||
if let Some(timeout) = self.timeout {
|
||||
@@ -676,7 +704,7 @@ impl Update {
|
||||
let proxy = reqwest::Proxy::all(proxy.as_str())?;
|
||||
request = request.proxy(proxy);
|
||||
}
|
||||
if let Some(ref configure_client) = self.configure_client {
|
||||
if let Some(ref configure_client) = self.context.configure_client {
|
||||
request = configure_client(request);
|
||||
}
|
||||
let response = request
|
||||
@@ -709,7 +737,7 @@ impl Update {
|
||||
}
|
||||
on_download_finish();
|
||||
|
||||
verify_signature(&buffer, &self.signature, &self.config.pubkey)?;
|
||||
verify_signature(&buffer, &self.signature, &self.context.config.pubkey)?;
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
@@ -733,6 +761,16 @@ impl Update {
|
||||
fn install_inner(&self, _bytes: &[u8]) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether the Windows installer should restart the app after installed, default is `true`
|
||||
#[cfg_attr(not(windows), allow(unused))]
|
||||
pub fn restart_after_install(mut self, restart_after_install: bool) -> Self {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
self.context.restart_after_install = restart_after_install;
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -785,7 +823,6 @@ impl Update {
|
||||
/// │ └──[AppName]_[version]_x64-setup.exe # NSIS installer
|
||||
/// └── ...
|
||||
fn install_inner(&self, bytes: &[u8]) -> Result<()> {
|
||||
use std::iter::once;
|
||||
use windows_sys::{
|
||||
w,
|
||||
Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_SHOW},
|
||||
@@ -793,48 +830,7 @@ impl Update {
|
||||
|
||||
let updater_type = self.extract(bytes)?;
|
||||
|
||||
let install_mode = self.config.install_mode();
|
||||
let current_args = &self.current_exe_args()[1..];
|
||||
let msi_args;
|
||||
let nsis_args;
|
||||
|
||||
let installer_args: Vec<&OsStr> = match &updater_type {
|
||||
WindowsUpdaterType::Nsis { .. } => {
|
||||
nsis_args = current_args
|
||||
.iter()
|
||||
.map(escape_nsis_current_exe_arg)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
install_mode
|
||||
.nsis_args()
|
||||
.iter()
|
||||
.map(OsStr::new)
|
||||
.chain(once(OsStr::new("/UPDATE")))
|
||||
.chain(once(OsStr::new("/ARGS")))
|
||||
.chain(nsis_args.iter().map(OsStr::new))
|
||||
.chain(self.installer_args())
|
||||
.collect()
|
||||
}
|
||||
WindowsUpdaterType::Msi { path, .. } => {
|
||||
let escaped_args = current_args
|
||||
.iter()
|
||||
.map(escape_msi_property_arg)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
msi_args = OsString::from(format!("LAUNCHAPPARGS=\"{escaped_args}\""));
|
||||
|
||||
[OsStr::new("/i"), path.as_os_str()]
|
||||
.into_iter()
|
||||
.chain(install_mode.msiexec_args().iter().map(OsStr::new))
|
||||
.chain(once(OsStr::new("/promptrestart")))
|
||||
.chain(self.installer_args())
|
||||
.chain(once(OsStr::new("AUTOLAUNCHAPP=True")))
|
||||
.chain(once(msi_args.as_os_str()))
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(on_before_exit) = self.on_before_exit.as_ref() {
|
||||
if let Some(on_before_exit) = self.context.on_before_exit.as_ref() {
|
||||
log::debug!("running on_before_exit hook");
|
||||
on_before_exit();
|
||||
}
|
||||
@@ -846,9 +842,11 @@ impl Update {
|
||||
|p| OsString::from(format!("{p}\\System32\\msiexec.exe")),
|
||||
),
|
||||
};
|
||||
let file = encode_wide(file);
|
||||
let parameters = self.updater_parameters(&updater_type);
|
||||
|
||||
let parameters = installer_args.join(OsStr::new(" "));
|
||||
log::debug!("Executing updater {file:?} with parameters: {parameters:?}");
|
||||
|
||||
let file = encode_wide(file);
|
||||
let parameters = encode_wide(parameters);
|
||||
|
||||
unsafe {
|
||||
@@ -865,18 +863,72 @@ impl Update {
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
fn installer_args(&self) -> Vec<&OsStr> {
|
||||
self.installer_args
|
||||
.iter()
|
||||
.map(OsStr::new)
|
||||
.collect::<Vec<_>>()
|
||||
fn updater_parameters(&self, updater_type: &WindowsUpdaterType) -> OsString {
|
||||
let install_mode = self.context.config.install_mode();
|
||||
let current_args = &self.context.current_exe_args[1..];
|
||||
|
||||
match updater_type {
|
||||
WindowsUpdaterType::Nsis { .. } => {
|
||||
let mut installer_args: Vec<&OsStr> = Vec::new();
|
||||
installer_args.extend(install_mode.nsis_args().iter().map(OsStr::new));
|
||||
installer_args.push(OsStr::new("/UPDATE"));
|
||||
|
||||
let nsis_current_exe_arg;
|
||||
if self.context.restart_after_install {
|
||||
nsis_current_exe_arg = current_args
|
||||
.iter()
|
||||
.map(escape_nsis_current_exe_arg)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
installer_args.extend(
|
||||
install_mode
|
||||
.nsis_restart_after_install_args()
|
||||
.iter()
|
||||
.map(OsStr::new),
|
||||
);
|
||||
installer_args.push(OsStr::new("/ARGS"));
|
||||
installer_args.extend(nsis_current_exe_arg.iter().map(OsStr::new));
|
||||
}
|
||||
|
||||
installer_args.extend(self.installer_args());
|
||||
|
||||
installer_args.join(OsStr::new(" "))
|
||||
}
|
||||
WindowsUpdaterType::Msi { path, .. } => {
|
||||
let mut installer_args: Vec<&OsStr> = vec![OsStr::new("/i"), path.as_os_str()];
|
||||
installer_args.extend(install_mode.msiexec_args().iter().map(OsStr::new));
|
||||
installer_args.push(OsStr::new("/promptrestart"));
|
||||
installer_args.extend(self.installer_args());
|
||||
|
||||
let msi_current_exe_arg;
|
||||
if self.context.restart_after_install {
|
||||
msi_current_exe_arg = format!(
|
||||
"LAUNCHAPPARGS=\"{}\"",
|
||||
current_args
|
||||
.iter()
|
||||
.map(escape_msi_property_arg)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
);
|
||||
|
||||
installer_args.extend(
|
||||
install_mode
|
||||
.msi_restart_after_install_args()
|
||||
.iter()
|
||||
.map(OsStr::new),
|
||||
);
|
||||
installer_args.push(OsStr::new(&msi_current_exe_arg));
|
||||
}
|
||||
|
||||
installer_args.join(OsStr::new(" "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn current_exe_args(&self) -> Vec<&OsStr> {
|
||||
self.current_exe_args
|
||||
.iter()
|
||||
.map(OsStr::new)
|
||||
.collect::<Vec<_>>()
|
||||
fn installer_args(
|
||||
&self,
|
||||
) -> std::iter::Map<std::slice::Iter<'_, OsString>, fn(&OsString) -> &OsStr> {
|
||||
self.context.installer_args.iter().map(OsStr::new)
|
||||
}
|
||||
|
||||
fn extract(&self, bytes: &[u8]) -> Result<WindowsUpdaterType> {
|
||||
@@ -890,7 +942,10 @@ impl Update {
|
||||
|
||||
fn make_temp_dir(&self) -> Result<PathBuf> {
|
||||
Ok(tempfile::Builder::new()
|
||||
.prefix(&format!("{}-{}-updater-", self.app_name, self.version))
|
||||
.prefix(&format!(
|
||||
"{}-{}-updater-",
|
||||
self.context.app_name, self.version
|
||||
))
|
||||
.tempdir()?
|
||||
.keep())
|
||||
}
|
||||
@@ -938,7 +993,10 @@ impl Update {
|
||||
|
||||
let temp_dir = self.make_temp_dir()?;
|
||||
let mut temp_file = tempfile::Builder::new()
|
||||
.prefix(&format!("{}-{}-installer", self.app_name, self.version))
|
||||
.prefix(&format!(
|
||||
"{}-{}-installer",
|
||||
self.context.app_name, self.version
|
||||
))
|
||||
.suffix(ext)
|
||||
.rand_bytes(0)
|
||||
.tempfile_in(temp_dir)?;
|
||||
@@ -1277,7 +1335,7 @@ impl Update {
|
||||
);
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let res = (self.run_on_main_thread)(Box::new(move || {
|
||||
let res = (self.context.run_on_main_thread)(Box::new(move || {
|
||||
let mut script =
|
||||
osakit::Script::new_from_source(osakit::Language::AppleScript, &apple_script);
|
||||
script.compile().expect("invalid AppleScript");
|
||||
@@ -1450,7 +1508,7 @@ where
|
||||
}
|
||||
|
||||
// Validate signature
|
||||
fn verify_signature(data: &[u8], release_signature: &str, pub_key: &str) -> Result<bool> {
|
||||
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)?;
|
||||
@@ -1459,7 +1517,7 @@ fn verify_signature(data: &[u8], release_signature: &str, pub_key: &str) -> Resu
|
||||
|
||||
// Validate signature or bail out
|
||||
public_key.verify(data, &signature, true)?;
|
||||
Ok(true)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn base64_to_string(base64_string: &str) -> Result<String> {
|
||||
@@ -1498,25 +1556,31 @@ impl PathExt for PathBuf {
|
||||
|
||||
// adapted from https://github.com/rust-lang/rust/blob/1c047506f94cd2d05228eb992b0a6bbed1942349/library/std/src/sys/args/windows.rs#L174
|
||||
#[cfg(windows)]
|
||||
fn escape_nsis_current_exe_arg(arg: &&OsStr) -> String {
|
||||
let arg = arg.to_string_lossy();
|
||||
let mut cmd: Vec<char> = Vec::new();
|
||||
fn escape_nsis_current_exe_arg(arg: impl AsRef<OsStr>) -> OsString {
|
||||
use std::os::windows::ffi::{OsStrExt, OsStringExt};
|
||||
|
||||
let arg = arg.as_ref();
|
||||
let mut cmd: Vec<u16> = Vec::new();
|
||||
|
||||
// compared to std we additionally escape `/` so that nsis won't interpret them as a beginning of an nsis argument.
|
||||
let quote = arg.chars().any(|c| c == ' ' || c == '\t' || c == '/') || arg.is_empty();
|
||||
let quote = arg
|
||||
.as_encoded_bytes()
|
||||
.iter()
|
||||
.any(|c| *c == b' ' || *c == b'\t' || *c == b'/')
|
||||
|| arg.is_empty();
|
||||
let escape = true;
|
||||
if quote {
|
||||
cmd.push('"');
|
||||
cmd.push('"' as u16);
|
||||
}
|
||||
let mut backslashes: usize = 0;
|
||||
for x in arg.chars() {
|
||||
for x in arg.encode_wide() {
|
||||
if escape {
|
||||
if x == '\\' {
|
||||
if x == '\\' as u16 {
|
||||
backslashes += 1;
|
||||
} else {
|
||||
if x == '"' {
|
||||
if x == '"' as u16 {
|
||||
// Add n+1 backslashes to total 2n+1 before internal '"'.
|
||||
cmd.extend((0..=backslashes).map(|_| '\\'));
|
||||
cmd.extend((0..=backslashes).map(|_| '\\' as u16));
|
||||
}
|
||||
backslashes = 0;
|
||||
}
|
||||
@@ -1525,10 +1589,10 @@ fn escape_nsis_current_exe_arg(arg: &&OsStr) -> String {
|
||||
}
|
||||
if quote {
|
||||
// Add n backslashes to total 2n before ending '"'.
|
||||
cmd.extend((0..backslashes).map(|_| '\\'));
|
||||
cmd.push('"');
|
||||
cmd.extend((0..backslashes).map(|_| '\\' as u16));
|
||||
cmd.push('"' as u16);
|
||||
}
|
||||
cmd.into_iter().collect()
|
||||
OsString::from_wide(&cmd)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
"endpoints": ["http://localhost:3007"],
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUwNDRGMjkwRjg2MDhCRDAKUldUUWkyRDRrUEpFNEQ4SmdwcU5PaXl6R2ZRUUNvUnhIaVkwVUltV0NMaEx6VTkrWVhpT0ZqeEEK",
|
||||
"windows": {
|
||||
"installMode": "quiet",
|
||||
"installerArgs": ["/NS"]
|
||||
"installMode": "quiet"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -46,7 +46,7 @@ struct Update {
|
||||
platforms: HashMap<String, PlatformUpdate>,
|
||||
}
|
||||
|
||||
fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTarget) {
|
||||
fn build_app(cwd: &Path, config: &Config, target: Option<BundleTarget>) {
|
||||
let mut command = Command::new("cargo");
|
||||
command
|
||||
.args(["tauri", "build", "--verbose"])
|
||||
@@ -56,17 +56,10 @@ fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTa
|
||||
.env("TAURI_SIGNING_PRIVATE_KEY_PASSWORD", "")
|
||||
.current_dir(cwd);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
command.args(["--bundles", target.name()]);
|
||||
#[cfg(target_os = "macos")]
|
||||
command.args(["--bundles", target.name()]);
|
||||
|
||||
if bundle_updater {
|
||||
#[cfg(windows)]
|
||||
command.args(["--bundles", "msi", "nsis"]);
|
||||
if let Some(target) = target {
|
||||
command.arg("--bundles").arg(target.name());
|
||||
} else {
|
||||
#[cfg(windows)]
|
||||
command.args(["--bundles", target.name()]);
|
||||
command.arg("--no-bundle");
|
||||
}
|
||||
|
||||
let status = command
|
||||
@@ -318,7 +311,7 @@ fn update_app() {
|
||||
{
|
||||
// bundle app update
|
||||
config.version = "1.0.0";
|
||||
build_app(&manifest_dir, &config, true, BundleTarget::default());
|
||||
build_app(&manifest_dir, &config, Some(bundle_target));
|
||||
|
||||
let bundle_updater_ext = if v1_compatible {
|
||||
out_bundle_path
|
||||
@@ -401,7 +394,7 @@ fn update_app() {
|
||||
config.version = "0.1.0";
|
||||
|
||||
// bundle initial app version
|
||||
build_app(&manifest_dir, &config, false, bundle_target);
|
||||
build_app(&manifest_dir, &config, None);
|
||||
|
||||
for expected_exit_code in status_checks {
|
||||
let mut binary_cmd = if cfg!(windows) {
|
||||
|
||||
+3
-3
@@ -3794,7 +3794,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4137,9 +4137,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wry"
|
||||
version = "0.24.10"
|
||||
version = "0.24.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00711278ed357350d44c749c286786ecac644e044e4da410d466212152383b45"
|
||||
checksum = "4a2a144c3ab5e83e04724bc8e67cea552ffae413185fda459fafdae173fd985d"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"block",
|
||||
|
||||
Reference in New Issue
Block a user