fix(macOS): Tauri V2 Update Permission Denied Error (#2067)

* WIP

* Fixed linting

* WIP

* Fixed linting

* use osakit to show actual app name on dialog

* sync versions

* lint

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
jLynx
2025-02-03 09:54:09 +13:00
committed by GitHub
parent a7497b0aeb
commit 5369898db7
6 changed files with 171 additions and 52 deletions
+1
View File
@@ -62,6 +62,7 @@ flate2 = { version = "1", optional = true }
[target."cfg(target_os = \"macos\")".dependencies]
tar = "0.4"
flate2 = "1"
osakit = { version = "0.3", features = ["full"] }
[features]
default = ["rustls-tls", "zip"]
+1 -7
View File
@@ -70,7 +70,6 @@ pub trait UpdaterExt<R: Runtime> {
impl<R: Runtime, T: Manager<R>> UpdaterExt<R> for T {
fn updater_builder(&self) -> UpdaterBuilder {
let app = self.app_handle();
let package_info = app.package_info();
let UpdaterState {
config,
target,
@@ -78,12 +77,7 @@ impl<R: Runtime, T: Manager<R>> UpdaterExt<R> for T {
headers,
} = self.state::<UpdaterState>().inner();
let mut builder = UpdaterBuilder::new(
package_info.name.clone(),
package_info.version.clone(),
config.clone(),
)
.headers(headers.clone());
let mut builder = UpdaterBuilder::new(app, config.clone()).headers(headers.clone());
if let Some(target) = target {
builder = builder.target(target);
+85 -29
View File
@@ -23,7 +23,7 @@ use reqwest::{
};
use semver::Version;
use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize};
use tauri::{utils::platform::current_exe, Resource};
use tauri::{utils::platform::current_exe, AppHandle, Resource, Runtime};
use time::OffsetDateTime;
use url::Url;
@@ -94,8 +94,13 @@ impl RemoteRelease {
pub type OnBeforeExit = Arc<dyn Fn() + Send + Sync + 'static>;
pub type VersionComparator = Arc<dyn Fn(Version, RemoteRelease) -> bool + Send + Sync>;
type MainThreadClosure = Box<dyn FnOnce() + Send + Sync + 'static>;
type RunOnMainThread =
Box<dyn Fn(MainThreadClosure) -> std::result::Result<(), tauri::Error> + Send + Sync + 'static>;
pub struct UpdaterBuilder {
#[allow(dead_code)]
run_on_main_thread: RunOnMainThread,
app_name: String,
current_version: Version,
config: Config,
@@ -112,18 +117,20 @@ pub struct UpdaterBuilder {
}
impl UpdaterBuilder {
/// It's prefered to use [`crate::UpdaterExt::updater_builder`] instead of
/// constructing a [`UpdaterBuilder`] with this function yourself
pub fn new(app_name: String, current_version: Version, config: crate::Config) -> Self {
pub(crate) fn new<R: Runtime>(app: &AppHandle<R>, config: crate::Config) -> Self {
let app_ = app.clone();
let run_on_main_thread =
move |f: Box<dyn FnOnce() + Send + Sync + 'static>| 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,
current_version,
app_name: app.package_info().name.clone(),
current_version: app.package_info().version.clone(),
config,
version_comparator: None,
executable_path: None,
@@ -259,6 +266,7 @@ 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,
@@ -291,6 +299,8 @@ impl UpdaterBuilder {
}
pub struct Updater {
#[allow(dead_code)]
run_on_main_thread: Arc<RunOnMainThread>,
config: Config,
app_name: String,
current_version: Version,
@@ -412,6 +422,7 @@ 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(),
@@ -440,6 +451,8 @@ 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>,
@@ -1031,42 +1044,85 @@ impl Update {
let cursor = Cursor::new(bytes);
let mut extracted_files: Vec<PathBuf> = Vec::new();
// the first file in the tar.gz will always be
// <app_name>/Contents
let tmp_dir = tempfile::Builder::new()
// Create temp directories for backup and extraction
let tmp_backup_dir = tempfile::Builder::new()
.prefix("tauri_current_app")
.tempdir()?;
// create backup of our current app
std::fs::rename(&self.extract_path, tmp_dir.path())?;
let tmp_extract_dir = tempfile::Builder::new()
.prefix("tauri_updated_app")
.tempdir()?;
let decoder = GzDecoder::new(cursor);
let mut archive = tar::Archive::new(decoder);
std::fs::create_dir(&self.extract_path)?;
// Extract files to temporary directory
for entry in archive.entries()? {
let mut entry = entry?;
// skip the first folder (should be the app name)
let collected_path: PathBuf = entry.path()?.iter().skip(1).collect();
let extraction_path = &self.extract_path.join(collected_path);
let extraction_path = tmp_extract_dir.path().join(&collected_path);
// if something went wrong during the extraction, we should restore previous app
if let Err(err) = entry.unpack(extraction_path) {
for file in extracted_files.iter().rev() {
// delete all the files we extracted
if file.is_dir() {
std::fs::remove_dir(file)?;
} else {
std::fs::remove_file(file)?;
}
}
std::fs::rename(tmp_dir.path(), &self.extract_path)?;
return Err(err.into());
// Ensure parent directories exist
if let Some(parent) = extraction_path.parent() {
std::fs::create_dir_all(parent)?;
}
extracted_files.push(extraction_path.to_path_buf());
if let Err(err) = entry.unpack(&extraction_path) {
// Cleanup on error
std::fs::remove_dir_all(tmp_extract_dir.path()).ok();
return Err(err.into());
}
extracted_files.push(extraction_path);
}
// Try to move the current app to backup
let move_result = std::fs::rename(
&self.extract_path,
tmp_backup_dir.path().join("current_app"),
);
let need_authorization = if let Err(err) = move_result {
if err.kind() == std::io::ErrorKind::PermissionDenied {
true
} else {
std::fs::remove_dir_all(tmp_extract_dir.path()).ok();
return Err(err.into());
}
} else {
false
};
if need_authorization {
// Use AppleScript to perform moves with admin privileges
let apple_script = format!(
"do shell script \"rm -rf '{src}' && mv -f '{new}' '{src}'\" with administrator privileges",
src = self.extract_path.display(),
new = tmp_extract_dir.path().display()
);
let (tx, rx) = std::sync::mpsc::channel();
let res = (self.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");
let r = script.execute();
tx.send(r).unwrap();
}));
let result = rx.recv().unwrap();
if res.is_err() || result.is_err() {
std::fs::remove_dir_all(tmp_extract_dir.path()).ok();
return Err(Error::Io(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"Failed to move the new app into place",
)));
}
} else {
// Remove existing directory if it exists
if self.extract_path.exists() {
std::fs::remove_dir_all(&self.extract_path)?;
}
// Move the new app to the target path
std::fs::rename(tmp_extract_dir.path(), &self.extract_path)?;
}
let _ = std::process::Command::new("touch")