fix(updater): fall back to the next temp location for AppImage updates (#3693)

- `tempdir_in(..)?` and `metadata()?` returned early on the first location
  that failed (e.g. a non-writable `/tmp`), so the cache dir and the
  directory next to the AppImage were never tried. Failures now move on to
  the next location, as the deb/rpm path already did.
- The candidate list was duplicated between the AppImage and deb/rpm paths;
  both now use `tmp_dir_locations`, which also no longer unwraps the parent
  of the app path (`extract_path.parent().unwrap()`).

Compile-checked with clippy in a Linux container (default features).

plugins/updater/src/updater.rs
This commit is contained in:
Lucas Fernandes Nogueira authored and GitHub committed 2026-10-11 16:14:50 -03:00
1 parent 21ebe1f531
commit 24e6451b11
2 files changed
+100 -88

No files matched your search

@@ -0,0 +1,6 @@
---
updater: patch
updater-js: patch
---
On Linux, try the next temporary directory location (the user cache directory, then the directory holding the AppImage) when a temporary directory cannot be created in the previous one, instead of failing the AppImage update right away. Also avoids a panic when the app path has no parent directory.
+94 -88
View File
@@ -1237,79 +1237,95 @@ impl Update {
return Err(Error::InvalidUpdaterFormat);
}
let tmp_dir_locations = vec![
Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>,
Box::new(dirs::cache_dir),
Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())),
];
for tmp_dir_location in tmp_dir_locations {
if let Some(tmp_dir_location) = tmp_dir_location() {
let tmp_dir = tempfile::Builder::new()
.prefix("tauri_current_app")
.tempdir_in(tmp_dir_location)?;
let tmp_dir_metadata = tmp_dir.path().metadata()?;
if extract_path_metadata.dev() == tmp_dir_metadata.dev() {
let mut perms = tmp_dir_metadata.permissions();
perms.set_mode(0o700);
std::fs::set_permissions(tmp_dir.path(), perms)?;
let tmp_app_image = &tmp_dir.path().join("current_app.AppImage");
let permissions = std::fs::metadata(&self.extract_path)?.permissions();
// create a backup of our current app image
match std::fs::rename(&self.extract_path, tmp_app_image) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::CrossesDevices => continue,
Err(err) => return Err(err.into()),
}
#[cfg(feature = "zip")]
if is_gz {
log::debug!("extracting AppImage");
// extract the buffer to the tmp_dir
// we extract our signed archive into our final directory without any temp file
let archive = std::io::Cursor::new(bytes);
let decoder = flate2::read::GzDecoder::new(archive);
let mut archive = tar::Archive::new(decoder);
for mut entry in archive.entries()?.flatten() {
if let Ok(path) = entry.path()
&& path.extension() == Some(OsStr::new("AppImage"))
{
// if something went wrong during the extraction, we should restore previous app
if let Err(err) = entry.unpack(&self.extract_path) {
std::fs::rename(tmp_app_image, &self.extract_path)?;
return Err(err.into());
}
// early finish we have everything we need here
return Ok(());
}
}
// if we have not returned early we should restore the backup
std::fs::rename(tmp_app_image, &self.extract_path)?;
return Err(Error::BinaryNotFoundInArchive);
}
log::debug!("rewriting AppImage");
return match std::fs::write(&self.extract_path, bytes)
.and_then(|_| std::fs::set_permissions(&self.extract_path, permissions))
{
Err(err) => {
// if something went wrong during the extraction, we should restore previous app
std::fs::rename(tmp_app_image, &self.extract_path)?;
Err(err.into())
}
Ok(_) => Ok(()),
};
for tmp_dir_location in self.tmp_dir_locations() {
let Some(tmp_dir_location) = tmp_dir_location() else {
continue;
};
// a location that cannot be used moves on to the next one
let tmp_dir = match tempfile::Builder::new()
.prefix("tauri_current_app")
.tempdir_in(&tmp_dir_location)
{
Ok(tmp_dir) => tmp_dir,
Err(err) => {
log::debug!(
"failed to create a temporary directory in {}: {err}",
tmp_dir_location.display()
);
continue;
}
};
if let Ok(tmp_dir_metadata) = tmp_dir.path().metadata()
&& extract_path_metadata.dev() == tmp_dir_metadata.dev()
{
let mut perms = tmp_dir_metadata.permissions();
perms.set_mode(0o700);
std::fs::set_permissions(tmp_dir.path(), perms)?;
let tmp_app_image = &tmp_dir.path().join("current_app.AppImage");
let permissions = std::fs::metadata(&self.extract_path)?.permissions();
// create a backup of our current app image
match std::fs::rename(&self.extract_path, tmp_app_image) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::CrossesDevices => continue,
Err(err) => return Err(err.into()),
}
#[cfg(feature = "zip")]
if is_gz {
log::debug!("extracting AppImage");
// extract the buffer to the tmp_dir
// we extract our signed archive into our final directory without any temp file
let archive = std::io::Cursor::new(bytes);
let decoder = flate2::read::GzDecoder::new(archive);
let mut archive = tar::Archive::new(decoder);
for mut entry in archive.entries()?.flatten() {
if let Ok(path) = entry.path()
&& path.extension() == Some(OsStr::new("AppImage"))
{
// if something went wrong during the extraction, we should restore previous app
if let Err(err) = entry.unpack(&self.extract_path) {
std::fs::rename(tmp_app_image, &self.extract_path)?;
return Err(err.into());
}
// early finish we have everything we need here
return Ok(());
}
}
// if we have not returned early we should restore the backup
std::fs::rename(tmp_app_image, &self.extract_path)?;
return Err(Error::BinaryNotFoundInArchive);
}
log::debug!("rewriting AppImage");
return match std::fs::write(&self.extract_path, bytes)
.and_then(|_| std::fs::set_permissions(&self.extract_path, permissions))
{
Err(err) => {
// if something went wrong during the extraction, we should restore previous app
std::fs::rename(tmp_app_image, &self.extract_path)?;
Err(err.into())
}
Ok(_) => Ok(()),
};
}
}
Err(Error::TempDirNotOnSameMountPoint)
}
/// Directories to try, in order, for the temporary files of an install: the system temporary
/// directory, the user cache directory and the directory holding the app.
fn tmp_dir_locations(&self) -> Vec<Box<dyn FnOnce() -> Option<PathBuf> + '_>> {
vec![
Box::new(|| Some(std::env::temp_dir())),
Box::new(dirs::cache_dir),
Box::new(|| self.extract_path.parent().map(Path::to_path_buf)),
]
}
fn install_deb(&self, bytes: &[u8]) -> Result<()> {
// First verify the bytes are actually a .deb package
if !infer::archive::is_deb(bytes) {
@@ -1335,31 +1351,21 @@ impl Update {
install_arg: &str,
package_extension: &str,
) -> Result<()> {
// Try different temp directories
let tmp_dir_locations = vec![
Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>,
Box::new(dirs::cache_dir),
Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())),
];
// Try writing to multiple temp locations until one succeeds
for tmp_dir_location in tmp_dir_locations {
if let Some(path) = tmp_dir_location() {
let prefix = format!("tauri_{package_extension}_update");
if let Ok(tmp_dir) = tempfile::Builder::new().prefix(&prefix).tempdir_in(path) {
let pkg_path = tmp_dir.path().join(format!("package.{package_extension}"));
for tmp_dir_location in self.tmp_dir_locations() {
let Some(path) = tmp_dir_location() else {
continue;
};
let prefix = format!("tauri_{package_extension}_update");
if let Ok(tmp_dir) = tempfile::Builder::new().prefix(&prefix).tempdir_in(path) {
let pkg_path = tmp_dir.path().join(format!("package.{package_extension}"));
// Try writing the .deb / .rpm file
if std::fs::write(&pkg_path, bytes).is_ok() {
// If write succeeds, proceed with installation
return self.try_install_with_privileges(
&pkg_path,
install_cmd,
install_arg,
);
}
// If write fails, continue to next temp location
// Try writing the .deb / .rpm file
if std::fs::write(&pkg_path, bytes).is_ok() {
// If write succeeds, proceed with installation
return self.try_install_with_privileges(&pkg_path, install_cmd, install_arg);
}
// If write fails, continue to next temp location
}
}