feat(updater): support non zipped updater (#1174)

pull/1267/head
Tony 1 year ago committed by GitHub
parent f39586bcb4
commit 1fa4d30eab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"updater": "patch"
---
Add support for updating using non-zipped files on Windows and Linux.

48
Cargo.lock generated

@ -334,16 +334,6 @@ dependencies = [
"zbus",
]
[[package]]
name = "assert-json-diff"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "async-broadcast"
version = "0.7.0"
@ -1146,16 +1136,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "colored"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8"
dependencies = [
"lazy_static",
"windows-sys 0.48.0",
]
[[package]]
name = "combine"
version = "4.6.6"
@ -1893,7 +1873,7 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee"
dependencies = [
"colored 1.9.4",
"colored",
"log",
]
@ -3469,24 +3449,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "mockito"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80f9fece9bd97ab74339fe19f4bcaf52b76dcc18e5364c7977c1838f76b38de9"
dependencies = [
"assert-json-diff",
"colored 2.1.0",
"httparse",
"lazy_static",
"log",
"rand 0.8.5",
"regex",
"serde_json",
"serde_urlencoded",
"similar",
]
[[package]]
name = "muda"
version = "0.13.1"
@ -5308,12 +5270,6 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
[[package]]
name = "similar"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21"
[[package]]
name = "single-instance-example"
version = "0.1.0"
@ -6460,8 +6416,8 @@ dependencies = [
"flate2",
"futures-util",
"http",
"infer",
"minisign-verify",
"mockito",
"reqwest",
"semver",
"serde",

@ -12,8 +12,8 @@
isChecking = true;
try {
const update = await check();
onMessage(`Should update: ${update.response.available}`);
onMessage(update.response);
onMessage(`Should update: ${update.available}`);
onMessage(update);
newUpdate = update;
} catch (e) {

@ -25,27 +25,33 @@ tokio = "1"
reqwest = { version = "0.12", default-features = false, features = [ "json", "stream" ] }
url = { workspace = true }
http = "1"
dirs-next = "2"
minisign-verify = "0.2"
time = { version = "0.3", features = [ "parsing", "formatting" ] }
base64 = "0.22"
semver = { version = "1", features = [ "serde" ] }
futures-util = "0.3"
tempfile = "3"
zip = "0.6"
infer = "0.15"
[target.'cfg(target_os = "windows")'.dependencies]
zip = { version = "0.6", optional = true }
windows-sys = { version = "0.52.0", features = [
"Win32_Foundation",
"Win32_UI_WindowsAndMessaging",
] }
[target."cfg(target_os = \"windows\")".dependencies]
windows-sys = { version = "0.52.0", features = [ "Win32_Foundation", "Win32_UI_WindowsAndMessaging" ] }
[target.'cfg(target_os = "linux")'.dependencies]
dirs-next = "2"
tar = { version = "0.4", optional = true }
flate2 = { version = "1", optional = true }
[target."cfg(any(target_os = \"macos\", target_os = \"linux\"))".dependencies]
[target.'cfg(target_os = "macos")'.dependencies]
tar = "0.4"
flate2 = "1"
[dev-dependencies]
mockito = "0.31"
[features]
default = [ "rustls-tls" ]
native-tls = [ "reqwest/native-tls" ]
native-tls-vendored = [ "reqwest/native-tls-vendored" ]
rustls-tls = [ "reqwest/rustls-tls" ]
default = ["rustls-tls", "zip"]
zip = ["dep:zip", "dep:tar", "dep:flate2"]
native-tls = ["reqwest/native-tls"]
native-tls-vendored = ["reqwest/native-tls-vendored"]
rustls-tls = ["reqwest/rustls-tls"]

@ -54,6 +54,7 @@ pub enum Error {
/// UTF8 Errors in signature.
#[error("The signature {0} could not be decoded, please check if it is a valid base64 string. The signature must be the contents of the `.sig` file generated by the Tauri bundler, as a string.")]
SignatureUtf8(String),
#[cfg(all(target_os = "windows", feature = "zip"))]
/// `zip` errors.
#[error(transparent)]
Extract(#[from] zip::result::ZipError),
@ -62,6 +63,8 @@ pub enum Error {
TempDirNotOnSameMountPoint,
#[error("binary for the current target not found in the archive")]
BinaryNotFoundInArchive,
#[error("invalid updater binary format")]
InvalidUpdaterFormat,
#[error(transparent)]
Http(#[from] http::Error),
#[error(transparent)]

@ -475,9 +475,8 @@ impl Update {
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
let bytes = chunk.as_ref().to_vec();
on_chunk(bytes.len(), content_length);
buffer.extend(bytes);
on_chunk(chunk.len(), content_length);
buffer.extend(chunk);
}
on_download_finish();
@ -508,114 +507,167 @@ impl Update {
fn install_inner(&self, _bytes: Vec<u8>) -> Result<()> {
Ok(())
}
}
#[cfg(windows)]
enum WindowsUpdaterType {
Nsis,
Msi,
}
// Windows
//
// ### Expected structure:
// ├── [AppName]_[version]_x64.msi.zip # ZIP generated by tauri-bundler
// │ └──[AppName]_[version]_x64.msi # Application MSI
// ├── [AppName]_[version]_x64-setup.exe.zip # ZIP generated by tauri-bundler
// │ └──[AppName]_[version]_x64-setup.exe # NSIS installer
// └── ...
//
// ## MSI
// Update server can provide a MSI for Windows. (Generated with tauri-bundler from *Wix*)
// To replace current version of the application. In later version we'll offer
// incremental update to push specific binaries.
//
// ## EXE
// Update server can provide a custom EXE (installer) who can run any task.
#[cfg(windows)]
#[cfg(windows)]
impl WindowsUpdaterType {
fn extension(&self) -> &str {
match self {
WindowsUpdaterType::Nsis => ".exe",
WindowsUpdaterType::Msi => ".msi",
}
}
}
#[cfg(windows)]
impl Config {
fn install_mode(&self) -> crate::config::WindowsUpdateInstallMode {
self.windows
.as_ref()
.map(|w| w.install_mode.clone())
.unwrap_or_default()
}
}
/// Windows
#[cfg(windows)]
impl Update {
/// ### Expected structure:
/// ├── [AppName]_[version]_x64.msi # Application MSI
/// ├── [AppName]_[version]_x64-setup.exe # NSIS installer
/// ├── [AppName]_[version]_x64.msi.zip # ZIP generated by tauri-bundler
/// │ └──[AppName]_[version]_x64.msi # Application MSI
/// ├── [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<()> {
use std::fs;
use windows_sys::{
w,
Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_SHOW},
};
// FIXME: We need to create a memory buffer with the MSI and then run it.
// (instead of extracting the MSI to a temp path)
//
// The tricky part is the MSI need to be exposed and spawned so the memory allocation
// shouldn't drop but we should be able to pass the reference so we can drop it once the installation
// is done, otherwise we have a huge memory leak.
let (updater_type, path, _temp) = Self::extract(&bytes)?;
let tmp_dir = tempfile::Builder::new().tempdir()?.into_path();
let archive = Cursor::new(bytes);
let mut extractor = zip::ZipArchive::new(archive)?;
extractor.extract(&tmp_dir)?;
let install_mode = self.config.install_mode();
let mut installer_args = self.installer_args();
match updater_type {
WindowsUpdaterType::Nsis => {
installer_args.extend(install_mode.nsis_args().iter().map(OsStr::new));
}
WindowsUpdaterType::Msi => {
installer_args.extend(install_mode.msiexec_args().iter().map(OsStr::new));
installer_args.push(OsStr::new("/promptrestart"));
}
};
let paths = fs::read_dir(&tmp_dir)?;
if let Some(on_before_exit) = self.on_before_exit.as_ref() {
on_before_exit();
}
let install_mode = self
.config
.windows
.as_ref()
.map(|w| w.install_mode.clone())
.unwrap_or_default();
let mut installer_args = self
.installer_args
let file = encode_wide(path);
let parameters = encode_wide(installer_args.join(OsStr::new(" ")));
unsafe {
ShellExecuteW(
0,
w!("open"),
file.as_ptr(),
parameters.as_ptr(),
std::ptr::null(),
SW_SHOW,
)
};
std::process::exit(0);
}
fn installer_args(&self) -> Vec<&OsStr> {
self.installer_args
.iter()
.map(OsStr::new)
.collect::<Vec<_>>();
.collect::<Vec<_>>()
}
fn extract(bytes: &[u8]) -> Result<(WindowsUpdaterType, PathBuf, Option<tempfile::TempPath>)> {
#[cfg(feature = "zip")]
if infer::archive::is_zip(bytes) {
return Self::extract_zip(bytes);
}
Self::extract_exe(bytes)
}
#[cfg(feature = "zip")]
fn extract_zip(
bytes: &[u8],
) -> Result<(WindowsUpdaterType, PathBuf, Option<tempfile::TempPath>)> {
let tmp_dir = tempfile::Builder::new().tempdir()?.into_path();
let archive = Cursor::new(bytes);
let mut extractor = zip::ZipArchive::new(archive)?;
extractor.extract(&tmp_dir)?;
let paths = std::fs::read_dir(&tmp_dir)?;
for path in paths {
let found_path = path?.path();
// we support 2 type of files exe & msi for now
// If it's an `exe` we expect an NSIS installer.
if found_path.extension() == Some(OsStr::new("exe")) {
installer_args.extend(install_mode.nsis_args().iter().map(OsStr::new));
} else if found_path.extension() == Some(OsStr::new("msi")) {
installer_args.extend(install_mode.msiexec_args().iter().map(OsStr::new));
installer_args.push(OsStr::new("/promptrestart"));
} else {
continue;
let ext = found_path.extension();
if ext == Some(OsStr::new("exe")) {
return Ok((WindowsUpdaterType::Nsis, found_path, None));
} else if ext == Some(OsStr::new("msi")) {
return Ok((WindowsUpdaterType::Msi, found_path, None));
}
}
if let Some(on_before_exit) = self.on_before_exit.as_ref() {
on_before_exit();
}
Err(crate::Error::BinaryNotFoundInArchive)
}
let file = encode_wide(found_path.as_os_str());
let parameters = encode_wide(installer_args.join(OsStr::new(" ")).as_os_str());
unsafe {
ShellExecuteW(
0,
w!("open"),
file.as_ptr(),
parameters.as_ptr(),
std::ptr::null(),
SW_SHOW,
)
};
fn extract_exe(
bytes: &[u8],
) -> Result<(WindowsUpdaterType, PathBuf, Option<tempfile::TempPath>)> {
use std::io::Write;
std::process::exit(0);
}
let updater_type = if infer::app::is_exe(bytes) {
WindowsUpdaterType::Nsis
} else if infer::archive::is_msi(bytes) {
WindowsUpdaterType::Msi
} else {
return Err(crate::Error::InvalidUpdaterFormat);
};
Ok(())
let ext = updater_type.extension();
let mut temp_file = tempfile::Builder::new().suffix(ext).tempfile()?;
temp_file.write_all(bytes)?;
let temp_path = temp_file.into_temp_path();
Ok((updater_type, temp_path.to_path_buf(), Some(temp_path)))
}
}
// Linux (AppImage)
//
// ### Expected structure:
// ├── [AppName]_[version]_amd64.AppImage.tar.gz # GZ generated by tauri-bundler
// │ └──[AppName]_[version]_amd64.AppImage # Application AppImage
// └── ...
//
// 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
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
/// Linux (AppImage)
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
impl Update {
/// ### Expected structure:
/// ├── [AppName]_[version]_amd64.AppImage.tar.gz # GZ generated by tauri-bundler
/// │ └──[AppName]_[version]_amd64.AppImage # Application AppImage
/// └── ...
///
/// 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<()> {
use flate2::read::GzDecoder;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let archive = Cursor::new(bytes);
let extract_path_metadata = self.extract_path.metadata()?;
let tmp_dir_locations = vec![
@ -641,43 +693,56 @@ impl Update {
// create a backup of our current app image
std::fs::rename(&self.extract_path, tmp_app_image)?;
// extract the buffer to the tmp_dir
// we extract our signed archive into our final directory without any temp file
let decoder = GzDecoder::new(archive);
let mut archive = tar::Archive::new(decoder);
for mut entry in archive.entries()?.flatten() {
if let Ok(path) = entry.path() {
if 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());
#[cfg(feature = "zip")]
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);
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() {
if 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(());
}
// 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);
}
// if we have not returned early we should restore the backup
std::fs::rename(tmp_app_image, &self.extract_path)?;
return Err(Error::BinaryNotFoundInArchive);
return match std::fs::write(&self.extract_path, bytes) {
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)
}
}
// MacOS
//
// ### Expected structure:
// ├── [AppName]_[version]_x64.app.tar.gz # GZ generated by tauri-bundler
// │ └──[AppName].app # Main application
// │ └── Contents # Application contents...
// │ └── ...
// └── ...
#[cfg(target_os = "macos")]
/// MacOS
#[cfg(target_os = "macos")]
impl Update {
/// ### Expected structure:
/// ├── [AppName]_[version]_x64.app.tar.gz # GZ generated by tauri-bundler
/// │ └──[AppName].app # Main application
/// │ └── Contents # Application contents...
/// │ └── ...
/// └── ...
fn install_inner(&self, bytes: Vec<u8>) -> Result<()> {
use flate2::read::GzDecoder;
@ -889,7 +954,7 @@ fn base64_to_string(base64_string: &str) -> Result<String> {
Ok(result)
}
#[cfg(target_os = "windows")]
#[cfg(windows)]
fn encode_wide(string: impl AsRef<OsStr>) -> Vec<u16> {
use std::os::windows::ffi::OsStrExt;

Loading…
Cancel
Save