Consolidate duplicate rust code (bins & lib-rust)

This commit is contained in:
Caelan
2024-09-26 11:31:50 -06:00
parent 9086b0d09f
commit 6ce3733976
42 changed files with 1317 additions and 1437 deletions

16
Cargo.lock generated
View File

@@ -2051,19 +2051,20 @@ checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
[[package]] [[package]]
name = "ts-rs" name = "ts-rs"
version = "9.0.1" version = "10.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b44017f9f875786e543595076374b9ef7d13465a518dd93d6ccdbf5b432dde8c" checksum = "3a2f31991cee3dce1ca4f929a8a04fdd11fd8801aac0f2030b0fa8a0a3fef6b9"
dependencies = [ dependencies = [
"lazy_static",
"thiserror", "thiserror",
"ts-rs-macros", "ts-rs-macros",
] ]
[[package]] [[package]]
name = "ts-rs-macros" name = "ts-rs-macros"
version = "9.0.1" version = "10.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c88cc88fd23b5a04528f3a8436024f20010a16ec18eb23c164b1242f65860130" checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2140,10 +2141,14 @@ name = "velopack"
version = "0.0.0-local" version = "0.0.0-local"
dependencies = [ dependencies = [
"async-std", "async-std",
"bitflags 2.6.0",
"derivative", "derivative",
"glob", "glob",
"lazy_static",
"log", "log",
"native-tls", "native-tls",
"normpath",
"regex",
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
@@ -2200,15 +2205,14 @@ dependencies = [
"time 0.3.36", "time 0.3.36",
"ureq", "ureq",
"url", "url",
"velopack",
"wait-timeout", "wait-timeout",
"waitpid-any", "waitpid-any",
"webview2-com", "webview2-com",
"windows", "windows",
"winres", "winres",
"winsafe", "winsafe",
"xml",
"zip", "zip",
"zstd",
] ]
[[package]] [[package]]

View File

@@ -31,15 +31,14 @@ name = "testapp"
path = "src/testapp.rs" path = "src/testapp.rs"
[dependencies] [dependencies]
velopack = { path = "../lib-rust" }
anyhow = "1.0" anyhow = "1.0"
pretty-bytes-rust = "0.3" pretty-bytes-rust = "0.3"
zip = { version = "2.1", default-features = false, features = ["deflate"] } zip = { version = "2.1", default-features = false, features = ["deflate"] }
regex = "1.10"
rand = "0.8" rand = "0.8"
log = "0.4" log = "0.4"
simplelog = "0.12" simplelog = "0.12"
clap = "4.5" clap = "4.5"
xml = "0.8"
semver = "1.0" semver = "1.0"
chrono = "0.4" chrono = "0.4"
wait-timeout = "0.2" wait-timeout = "0.2"
@@ -58,7 +57,6 @@ enum-flags = "0.3"
remove_dir_all = { git = "https://github.com/caesay/remove_dir_all.git", features = [ remove_dir_all = { git = "https://github.com/caesay/remove_dir_all.git", features = [
"log", "log",
] } ] }
zstd = "0.13"
sha1_smol = "1.0" sha1_smol = "1.0"
url = "2.5" url = "2.5"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
@@ -66,6 +64,7 @@ serde_json = "1.0"
time = "0.3" time = "0.3"
os_info = "3.8" os_info = "3.8"
bitflags = "2.6" bitflags = "2.6"
regex = "1.10"
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
native-dialog = "0.7" native-dialog = "0.7"

View File

@@ -1,7 +1,7 @@
use crate::{ use crate::{
bundle, shared::{self, OperationWait},
shared::{self, bundle::Manifest, OperationWait},
}; };
use velopack::{bundle::load_bundle_from_file, bundle::Manifest, locator::VelopackLocator};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use std::path::PathBuf; use std::path::PathBuf;
@@ -13,26 +13,29 @@ use super::apply_osx_impl::apply_package_impl;
use super::apply_windows_impl::apply_package_impl; use super::apply_windows_impl::apply_package_impl;
pub fn apply<'a>( pub fn apply<'a>(
root_path: &PathBuf, locator: &VelopackLocator,
app: &Manifest,
restart: bool, restart: bool,
wait: OperationWait, wait: OperationWait,
package: Option<&PathBuf>, package: Option<&PathBuf>,
exe_args: Option<Vec<&str>>, exe_args: Option<Vec<&str>>,
runhooks: bool, run_hooks: bool,
) -> Result<()> { ) -> Result<()> {
shared::operation_wait(wait); shared::operation_wait(wait);
let package = package.cloned().map_or_else(|| auto_locate_package(&app, &root_path), Ok); let package = package.cloned().map_or_else(|| auto_locate_package(&locator), Ok);
match package { match package {
Ok(package) => { Ok(package) => {
info!("Getting ready to apply package to {} ver {}: {}", app.id, app.version, package.to_string_lossy()); info!("Getting ready to apply package to {} ver {}: {}",
match apply_package_impl(&root_path, &app, &package, runhooks) { locator.get_manifest_id(),
Ok(applied_app) => { locator.get_manifest_version_full_string(),
info!("Package version {} applied successfully.", applied_app.version); package.to_string_lossy());
match apply_package_impl(&locator, &package, run_hooks) {
Ok(applied_locator) => {
info!("Package version {} applied successfully.", applied_locator.get_manifest_version_full_string());
// if successful, we want to restart the new version of the app, which could have different metadata // if successful, we want to restart the new version of the app, which could have different metadata
if restart { if restart {
shared::start_package(&applied_app, &root_path, exe_args, Some("VELOPACK_RESTART"))?; shared::start_package(&applied_locator, exe_args, Some("VELOPACK_RESTART"))?;
} }
return Ok(()); return Ok(());
} }
@@ -48,20 +51,14 @@ pub fn apply<'a>(
// an error occurred if we're here, but we still want to restart the old version of the app if it was requested // an error occurred if we're here, but we still want to restart the old version of the app if it was requested
if restart { if restart {
shared::start_package(&app, &root_path, exe_args, Some("VELOPACK_RESTART"))?; shared::start_package(&locator, exe_args, Some("VELOPACK_RESTART"))?;
} }
bail!("Apply failed, see logs for details."); bail!("Apply failed, see logs for details.");
} }
fn auto_locate_package(app: &Manifest, _root_path: &PathBuf) -> Result<PathBuf> { fn auto_locate_package(locator: &VelopackLocator) -> Result<PathBuf> {
#[cfg(target_os = "windows")] let packages_dir = locator.get_packages_dir_as_string();
let packages_dir = app.get_packages_path(_root_path);
#[cfg(target_os = "linux")]
let packages_dir = format!("/var/tmp/velopack/{}/packages", &app.id);
#[cfg(target_os = "macos")]
let packages_dir = format!("/tmp/velopack/{}/packages", &app.id);
info!("Attempting to auto-detect package in: {}", packages_dir); info!("Attempting to auto-detect package in: {}", packages_dir);
let mut package_path: Option<PathBuf> = None; let mut package_path: Option<PathBuf> = None;
let mut package_manifest: Option<Manifest> = None; let mut package_manifest: Option<Manifest> = None;
@@ -70,7 +67,7 @@ fn auto_locate_package(app: &Manifest, _root_path: &PathBuf) -> Result<PathBuf>
for path in paths { for path in paths {
if let Ok(path) = path { if let Ok(path) = path {
trace!("Checking package: '{}'", path.to_string_lossy()); trace!("Checking package: '{}'", path.to_string_lossy());
if let Ok(bun) = bundle::load_bundle_from_file(&path) { if let Ok(mut bun) = load_bundle_from_file(&path) {
if let Ok(mani) = bun.read_manifest() { if let Ok(mani) = bun.read_manifest() {
if package_manifest.is_none() || mani.version > package_manifest.clone().unwrap().version { if package_manifest.is_none() || mani.version > package_manifest.clone().unwrap().version {
info!("Found {}: '{}'", mani.version, path.to_string_lossy()); info!("Found {}: '{}'", mani.version, path.to_string_lossy());
@@ -84,7 +81,7 @@ fn auto_locate_package(app: &Manifest, _root_path: &PathBuf) -> Result<PathBuf>
} }
if let Some(p) = package_path { if let Some(p) = package_path {
return Ok(p); Ok(p)
} else { } else {
bail!("Unable to find/load suitable package. Provide via the --package argument."); bail!("Unable to find/load suitable package. Provide via the --package argument.");
} }

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
dialogs, dialogs,
shared::{self, bundle, bundle::Manifest}, shared::{self},
windows::locksmith, windows::locksmith,
windows::splash, windows::splash,
}; };
@@ -10,6 +10,7 @@ use std::{
fs, fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use velopack::{bundle::load_bundle_from_file, locator::VelopackLocator};
fn ropycopy<P1: AsRef<Path>, P2: AsRef<Path>>(source: &P1, dest: &P2) -> Result<()> { fn ropycopy<P1: AsRef<Path>, P2: AsRef<Path>>(source: &P1, dest: &P2) -> Result<()> {
let source = source.as_ref(); let source = source.as_ref();
@@ -41,28 +42,31 @@ fn ropycopy<P1: AsRef<Path>, P2: AsRef<Path>>(source: &P1, dest: &P2) -> Result<
Ok(()) Ok(())
} }
pub fn apply_package_impl(root_path: &PathBuf, old_app: &Manifest, package: &PathBuf, run_hooks: bool) -> Result<Manifest> { pub fn apply_package_impl(old_locator: &VelopackLocator, package: &PathBuf, run_hooks: bool) -> Result<VelopackLocator> {
let bundle = bundle::load_bundle_from_file(package)?; let mut bundle = load_bundle_from_file(package)?;
let new_app = bundle.read_manifest()?; let new_app_manifest = bundle.read_manifest()?;
let new_locator = old_locator.clone_self_with_new_manifest(&new_app_manifest);
let found_version = (new_app.version).to_owned(); let root_path = old_locator.get_root_dir();
info!("Applying package to current: {} (old version {})", found_version, old_app.version); let old_version = old_locator.get_manifest_version();
let new_version = new_locator.get_manifest_version();
if !crate::windows::prerequisite::prompt_and_install_all_missing(&new_app, Some(&old_app.version))? { info!("Applying package {} to current: {}", new_version, old_version);
if !crate::windows::prerequisite::prompt_and_install_all_missing(&new_app_manifest, Some(&old_version))? {
bail!("Stopping apply. Pre-requisites are missing and user cancelled."); bail!("Stopping apply. Pre-requisites are missing and user cancelled.");
} }
let packages_dir = old_app.get_packages_path(root_path); let packages_dir = old_locator.get_packages_dir();
let packages_dir = Path::new(&packages_dir); let current_dir = old_locator.get_current_bin_dir();
let current_dir = old_app.get_current_path(root_path);
let temp_path_new = packages_dir.join(format!("tmp_{}", shared::random_string(16))); let temp_path_new = packages_dir.join(format!("tmp_{}", shared::random_string(16)));
let temp_path_old = packages_dir.join(format!("tmp_{}", shared::random_string(16))); let temp_path_old = packages_dir.join(format!("tmp_{}", shared::random_string(16)));
// open a dialog showing progress... // open a dialog showing progress...
let (mut tx, _) = mpsc::channel::<i16>(); let (mut tx, _) = mpsc::channel::<i16>();
if !dialogs::get_silent() { if !dialogs::get_silent() {
let title = format!("{} Update", &new_app.title); let title = format!("{} Update", new_locator.get_manifest_title());
let message = format!("Installing update {}...", &new_app.version); let message = format!("Installing update {}...", new_locator.get_manifest_version_full_string());
tx = splash::show_progress_dialog(title, message); tx = splash::show_progress_dialog(title, message);
} }
@@ -77,14 +81,14 @@ pub fn apply_package_impl(root_path: &PathBuf, old_app: &Manifest, package: &Pat
// second, run application hooks (but don't care if it fails) // second, run application hooks (but don't care if it fails)
if run_hooks { if run_hooks {
crate::windows::run_hook(old_app, root_path, "--veloapp-obsolete", 15); crate::windows::run_hook(old_locator, "--veloapp-obsolete", 15);
} else { } else {
info!("Skipping --veloapp-obsolete hook."); info!("Skipping --veloapp-obsolete hook.");
} }
// third, we try _REALLY HARD_ to stop the package // third, we try _REALLY HARD_ to stop the package
let _ = shared::force_stop_package(root_path); let _ = shared::force_stop_package(root_path);
if winsafe::IsWindows10OrGreater() == Ok(true) && !locksmith::close_processes_locking_dir(&new_app, &current_dir) { if winsafe::IsWindows10OrGreater() == Ok(true) && !locksmith::close_processes_locking_dir(&old_locator) {
bail!("Failed to close processes locking directory / user cancelled."); bail!("Failed to close processes locking directory / user cancelled.");
} }
@@ -116,10 +120,12 @@ pub fn apply_package_impl(root_path: &PathBuf, old_app: &Manifest, package: &Pat
let _ = tx.send(splash::MSG_CLOSE); let _ = tx.send(splash::MSG_CLOSE);
info!("Showing error dialog..."); info!("Showing error dialog...");
let title = format!("{} Update", &new_app.title); let title = format!("{} Update", &new_locator.get_manifest_title());
let header = "Failed to update"; let header = "Failed to update";
let body = let body =
format!("Failed to update {} to version {}. Please check the logs for more details.", &new_app.title, &new_app.version); format!("Failed to update {} to version {}. Please check the logs for more details.",
&new_locator.get_manifest_title(),
&new_locator.get_manifest_version_full_string());
dialogs::show_error(&title, Some(header), &body); dialogs::show_error(&title, Some(header), &body);
bail!("Fatal error performing update."); bail!("Fatal error performing update.");
@@ -128,13 +134,23 @@ pub fn apply_package_impl(root_path: &PathBuf, old_app: &Manifest, package: &Pat
// from this point on, we're past the point of no return and should not bail // from this point on, we're past the point of no return and should not bail
// sixth, we write the uninstall entry // sixth, we write the uninstall entry
if let Err(e) = new_app.write_uninstall_entry(root_path) { if !old_locator.get_is_portable() {
warn!("Failed to write uninstall entry ({}).", e); if old_locator.get_manifest_id() != new_locator.get_manifest_id() {
info!("The app ID has changed, removing old uninstall registry entry.");
if let Err(e) = crate::windows::registry::remove_uninstall_entry(&old_locator) {
warn!("Failed to remove old uninstall entry ({}).", e);
}
}
if let Err(e) = crate::windows::registry::write_uninstall_entry(&new_locator) {
warn!("Failed to write new uninstall entry ({}).", e);
}
} else {
info!("Skipping uninstall entry for portable app.");
} }
// seventh, we run the post-install hooks // seventh, we run the post-install hooks
if run_hooks { if run_hooks {
crate::windows::run_hook(&new_app, &root_path, "--veloapp-updated", 15); crate::windows::run_hook(&new_locator, "--veloapp-updated", 15);
} else { } else {
info!("Skipping --veloapp-updated hook."); info!("Skipping --veloapp-updated hook.");
} }
@@ -145,7 +161,8 @@ pub fn apply_package_impl(root_path: &PathBuf, old_app: &Manifest, package: &Pat
// to update the shortcut to point at the temp/renamed location // to update the shortcut to point at the temp/renamed location
let _ = remove_dir_all::remove_dir_all(&temp_path_new); let _ = remove_dir_all::remove_dir_all(&temp_path_new);
let _ = remove_dir_all::remove_dir_all(&temp_path_old); let _ = remove_dir_all::remove_dir_all(&temp_path_old);
crate::windows::create_or_update_manifest_lnks(root_path, &new_app, Some(old_app));
crate::windows::create_or_update_manifest_lnks(&new_locator, Some(old_locator));
// done! // done!
info!("Package applied successfully."); info!("Package applied successfully.");
@@ -156,5 +173,5 @@ pub fn apply_package_impl(root_path: &PathBuf, old_app: &Manifest, package: &Pat
let _ = remove_dir_all::remove_dir_all(&temp_path_new); let _ = remove_dir_all::remove_dir_all(&temp_path_new);
let _ = remove_dir_all::remove_dir_all(&temp_path_old); let _ = remove_dir_all::remove_dir_all(&temp_path_old);
action?; action?;
Ok(new_app) Ok(new_locator)
} }

View File

@@ -1,33 +1,21 @@
use crate::{ use crate::{
dialogs, dialogs,
shared::{self, bundle, runtime_arch::RuntimeArch}, shared::{self},
windows, windows,
}; };
use velopack::bundle::BundleZip;
use velopack::locator::*;
use anyhow::{anyhow, bail, Result};
use pretty_bytes_rust::pretty_bytes;
use std::{
fs::{self},
path::{Path, PathBuf},
};
use ::windows::core::PCWSTR; use ::windows::core::PCWSTR;
use ::windows::Win32::Storage::FileSystem::GetDiskFreeSpaceExW; use ::windows::Win32::Storage::FileSystem::GetDiskFreeSpaceExW;
use anyhow::{anyhow, bail, Result};
use memmap2::Mmap;
use pretty_bytes_rust::pretty_bytes;
use std::{
env,
fs::{self, File},
path::{Path, PathBuf},
};
pub fn install(debug_pkg: Option<&PathBuf>, install_to: Option<&PathBuf>) -> Result<()> {
let osinfo = os_info::get();
let osarch = RuntimeArch::from_current_system();
info!("OS: {osinfo}, Arch={osarch:#?}");
if !windows::is_windows_7_sp1_or_greater() {
bail!("This installer requires Windows 7 SPA1 or later and cannot run.");
}
let file = File::open(env::current_exe()?)?;
let mmap = unsafe { Mmap::map(&file)? };
let pkg = bundle::load_bundle_from_mmap(&mmap, debug_pkg)?;
info!("Bundle loaded successfully.");
pub fn install(pkg: &mut BundleZip, install_to: Option<&PathBuf>, start_args: Option<Vec<&str>>) -> Result<()> {
// find and parse nuspec // find and parse nuspec
info!("Reading package manifest..."); info!("Reading package manifest...");
let app = pkg.read_manifest()?; let app = pkg.read_manifest()?;
@@ -41,7 +29,7 @@ pub fn install(debug_pkg: Option<&PathBuf>, install_to: Option<&PathBuf>) -> Res
info!(" Package Machine Architecture: {}", &app.machine_architecture); info!(" Package Machine Architecture: {}", &app.machine_architecture);
info!(" Package Runtime Dependencies: {}", &app.runtime_dependencies); info!(" Package Runtime Dependencies: {}", &app.runtime_dependencies);
let _mutex = shared::retry_io(|| windows::create_global_mutex(&app))?; let _mutex = shared::retry_io(|| windows::create_global_mutex(&app.id))?;
if !windows::prerequisite::prompt_and_install_all_missing(&app, None)? { if !windows::prerequisite::prompt_and_install_all_missing(&app, None)? {
info!("Cancelling setup. Pre-requisites not installed."); info!("Cancelling setup. Pre-requisites not installed.");
@@ -136,7 +124,7 @@ pub fn install(debug_pkg: Option<&PathBuf>, install_to: Option<&PathBuf>) -> Res
windows::splash::show_splash_dialog(app.title.to_owned(), splash_bytes) windows::splash::show_splash_dialog(app.title.to_owned(), splash_bytes)
}; };
let install_result = install_impl(&pkg, &root_path, &tx); let install_result = install_impl(pkg, &root_path, &tx, start_args);
let _ = tx.send(windows::splash::MSG_CLOSE); let _ = tx.send(windows::splash::MSG_CLOSE);
if install_result.is_ok() { if install_result.is_ok() {
@@ -159,17 +147,19 @@ pub fn install(debug_pkg: Option<&PathBuf>, install_to: Option<&PathBuf>) -> Res
Ok(()) Ok(())
} }
fn install_impl(pkg: &bundle::BundleInfo, root_path: &PathBuf, tx: &std::sync::mpsc::Sender<i16>) -> Result<()> { fn install_impl(pkg: &mut BundleZip, root_path: &PathBuf, tx: &std::sync::mpsc::Sender<i16>, start_args: Option<Vec<&str>>) -> Result<()> {
info!("Starting installation!"); info!("Starting installation!");
let app = pkg.read_manifest()?; let app_manifest = pkg.read_manifest()?;
let paths = create_config_from_root_dir(root_path);
let locator = VelopackLocator::new(paths, app_manifest);
// all application paths // all application paths
let updater_path = app.get_update_path(root_path); let updater_path = locator.get_update_path();
let packages_path = app.get_packages_path(root_path); let packages_path = locator.get_packages_dir();
let current_path = app.get_current_path(root_path); let current_path = locator.get_current_bin_dir();
let nupkg_path = app.get_target_nupkg_path(root_path); let nupkg_path = locator.get_ideal_local_nupkg_path(None, None);
let main_exe_path = app.get_main_exe_path(root_path); let main_exe_path = locator.get_main_exe_path();
info!("Extracting Update.exe..."); info!("Extracting Update.exe...");
let _ = pkg let _ = pkg
@@ -186,18 +176,18 @@ fn install_impl(pkg: &bundle::BundleInfo, root_path: &PathBuf, tx: &std::sync::m
let _ = tx.send(((p as f32) / 100.0 * 80.0 + 10.0) as i16); let _ = tx.send(((p as f32) / 100.0 * 80.0 + 10.0) as i16);
})?; })?;
if !Path::new(&main_exe_path).exists() { if !main_exe_path.exists() {
bail!("The main executable could not be found in the package. Please contact the application author."); bail!("The main executable could not be found in the package. Please contact the application author.");
} }
info!("Creating shortcuts..."); if locator.get_manifest_shortcut_locations() != ShortcutLocationFlags::NONE {
if !app.shortcut_locations.is_empty() { info!("Creating shortcuts...");
windows::create_or_update_manifest_lnks(&root_path, &app, None); windows::create_or_update_manifest_lnks(&locator, None);
} }
info!("Starting process install hook"); info!("Starting process install hook");
if !windows::run_hook(&app, &root_path, "--veloapp-install", 30) { if !windows::run_hook(&locator, "--veloapp-install", 30) {
let setup_name = format!("{} Setup {}", app.title, app.version); let setup_name = format!("{} Setup {}", locator.get_manifest_title(), locator.get_manifest_id());
dialogs::show_warn( dialogs::show_warn(
&setup_name, &setup_name,
None, None,
@@ -206,11 +196,11 @@ fn install_impl(pkg: &bundle::BundleInfo, root_path: &PathBuf, tx: &std::sync::m
} }
let _ = tx.send(100); let _ = tx.send(100);
app.write_uninstall_entry(root_path)?; windows::registry::write_uninstall_entry(&locator)?;
if !dialogs::get_silent() { if !dialogs::get_silent() {
info!("Starting app..."); info!("Starting app...");
shared::start_package(&app, &root_path, None, Some("VELOPACK_FIRSTRUN"))?; shared::start_package(&locator, start_args, Some("VELOPACK_FIRSTRUN"))?;
} }
Ok(()) Ok(())

View File

@@ -1,9 +1,6 @@
mod apply; mod apply;
pub use apply::*; pub use apply::*;
mod patch;
pub use patch::*;
mod start; mod start;
pub use start::*; pub use start::*;

View File

@@ -1,43 +0,0 @@
use anyhow::{bail, Result};
use std::{fs, io, path::PathBuf};
pub fn patch(old_file: &PathBuf, patch_file: &PathBuf, output_file: &PathBuf) -> Result<()> {
if !old_file.exists() {
bail!("Old file does not exist: {}", old_file.to_string_lossy());
}
if !patch_file.exists() {
bail!("Patch file does not exist: {}", patch_file.to_string_lossy());
}
let dict = fs::read(old_file)?;
info!("Loading Dictionary (Size: {})", dict.len());
let patch = fs::OpenOptions::new().read(true).open(patch_file)?;
let patch_reader = io::BufReader::new(patch);
let mut decoder = zstd::Decoder::with_dictionary(patch_reader, &dict)?;
let window_log = fio_highbit64(dict.len() as u64) + 1;
if window_log >= 27 {
info!("Large File detected. Overriding windowLog to {}", window_log);
decoder.window_log_max(window_log)?;
}
info!("Decoder loaded. Beginning patch...");
let mut output = fs::OpenOptions::new().write(true).create(true).open(output_file)?;
io::copy(&mut decoder, &mut output)?;
info!("Patch applied successfully.");
Ok(())
}
fn fio_highbit64(v: u64) -> u32 {
let mut count: u32 = 0;
let mut v = v;
v >>= 1;
while v > 0 {
v >>= 1;
count += 1;
}
return count;
}

View File

@@ -1,14 +1,10 @@
use std::path::PathBuf;
use anyhow::Result;
use crate::bundle::Manifest;
use crate::shared::{self, OperationWait}; use crate::shared::{self, OperationWait};
use anyhow::Result;
use velopack::locator::VelopackLocator;
#[allow(unused_variables, unused_imports)] #[allow(unused_variables, unused_imports)]
pub fn start( pub fn start(
root_dir: &PathBuf, locator: &VelopackLocator,
app: &Manifest,
wait: OperationWait, wait: OperationWait,
exe_name: Option<&String>, exe_name: Option<&String>,
exe_args: Option<Vec<&str>>, exe_args: Option<Vec<&str>>,
@@ -24,7 +20,7 @@ pub fn start(
shared::operation_wait(wait); shared::operation_wait(wait);
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
super::start_windows_impl::start_impl(&root_dir, &app, exe_name, exe_args, legacy_args)?; super::start_windows_impl::start_impl(&locator, exe_name, exe_args, legacy_args)?;
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
shared::start_package(&app, &root_dir, exe_args, None)?; shared::start_package(&app, &root_dir, exe_args, None)?;

View File

@@ -1,67 +1,71 @@
use crate::bundle::Manifest;
use crate::{ use crate::{
dialogs, dialogs,
shared::{self, bundle, OperationWait}, shared::{self, OperationWait},
windows as win, windows as win,
}; };
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
use std::{ use std::{
fs, fs,
path::{Path, PathBuf}, path::Path,
process::Command as Process, process::Command as Process,
}; };
use velopack::bundle;
use velopack::locator::VelopackLocator;
use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow; use windows::Win32::UI::WindowsAndMessaging::AllowSetForegroundWindow;
pub fn start_impl( pub fn start_impl(
root_dir: &PathBuf, locator: &VelopackLocator,
app: &Manifest,
exe_name: Option<&String>, exe_name: Option<&String>,
exe_args: Option<Vec<&str>>, exe_args: Option<Vec<&str>>,
legacy_args: Option<&String>, legacy_args: Option<&String>,
) -> Result<()> { ) -> Result<()> {
match shared::has_app_prefixed_folder(root_dir) { let root_dir = locator.get_root_dir();
let app_title = locator.get_manifest_title();
match shared::has_app_prefixed_folder(&root_dir) {
Ok(has_prefix) => { Ok(has_prefix) => {
if has_prefix { if has_prefix {
info!("This is a legacy app. Will try and upgrade it now."); info!("This is a legacy app. Will try and upgrade it now.");
// if started by legacy Squirrel, the working dir of Update.exe may be inside the app-* folder, // if started by legacy Squirrel, the working dir of Update.exe may be inside the app-* folder,
// meaning we can not clean up properly. // meaning we can not clean up properly.
std::env::set_current_dir(root_dir)?; std::env::set_current_dir(&root_dir)?;
if let Err(e) = try_legacy_migration(root_dir, app) { return match try_legacy_migration(&locator) {
warn!("Failed to migrate legacy app ({}).", e); Ok(new_locator) => {
dialogs::show_error( shared::start_package(&new_locator, exe_args, Some("VELOPACK_RESTART"))?;
&app.title, Ok(())
Some("Unable to start app"), }
"This app installation has been corrupted and cannot be started. Please reinstall the app.", Err(e) => {
); warn!("Failed to migrate legacy app ({}).", e);
return Err(e); dialogs::show_error(
&app_title,
Some("Unable to start app"),
"This app installation has been corrupted and cannot be started. Please reinstall the app.",
);
Err(e)
}
} }
// we can't run the normal start command, because legacy squirrel might provide an "exe name" to restart
// which no longer exists in the package
let (root_dir, app) = shared::detect_current_manifest()?;
shared::start_package(&app, root_dir, exe_args, Some("VELOPACK_RESTART"))?;
return Ok(());
} }
} }
Err(e) => warn!("Failed legacy check ({}).", e), Err(e) => warn!("Failed legacy check ({}).", e),
} }
let current = app.get_current_path(root_dir); // we can't just run the normal start_package command, because legacy squirrel might provide
// an "exe name" to restart which no longer exists in the package
let current = locator.get_current_bin_dir();
let exe_to_execute = if let Some(exe) = exe_name { let exe_to_execute = if let Some(exe) = exe_name {
Path::new(&current).join(exe) Path::new(&current).join(exe)
} else { } else {
let exe = app.get_main_exe_path(root_dir); locator.get_main_exe_path()
Path::new(&exe).to_path_buf()
}; };
if !exe_to_execute.exists() { if !exe_to_execute.exists() {
bail!("Unable to find executable to start: '{}'", exe_to_execute.to_string_lossy()); bail!("Unable to find executable to start: '{:?}'", exe_to_execute);
} }
info!("About to launch: '{}' in dir '{}'", exe_to_execute.to_string_lossy(), current); info!("About to launch: '{:?}' in dir '{:?}'", exe_to_execute, current);
let mut cmd = Process::new(&exe_to_execute); let mut cmd = Process::new(&exe_to_execute);
cmd.current_dir(&current); cmd.current_dir(&current);
@@ -77,34 +81,37 @@ pub fn start_impl(
Ok(()) Ok(())
} }
fn try_legacy_migration(root_dir: &PathBuf, app: &bundle::Manifest) -> Result<()> { fn try_legacy_migration(locator: &VelopackLocator) -> Result<VelopackLocator> {
let package = shared::find_latest_full_package(root_dir).ok_or_else(|| anyhow!("Unable to find latest full package."))?; let root_dir = locator.get_root_dir();
let bundle = bundle::load_bundle_from_file(&package.file_path)?; let current_dir = locator.get_current_bin_dir();
let _bundle_manifest = bundle.read_manifest()?; // this verifies it's a bundle we support let package = shared::find_latest_full_package(&locator).ok_or_else(|| anyhow!("Unable to find latest full package."))?;
let mut bundle = bundle::load_bundle_from_file(&package.file_path)?;
let bundle_manifest = bundle.read_manifest()?; // this verifies it's a bundle we support
warn!("This application is installed in a folder prefixed with 'app-'. Attempting to migrate..."); warn!("This application is installed in a folder prefixed with 'app-'. Attempting to migrate...");
let _ = shared::force_stop_package(root_dir); let _ = shared::force_stop_package(&root_dir);
let current_dir = app.get_current_path(root_dir);
if !Path::new(&current_dir).exists() { if !Path::new(&current_dir).exists() {
info!("Renaming latest app-* folder to current."); info!("Renaming latest app-* folder to current.");
if let Some((latest_app_dir, _latest_ver)) = shared::get_latest_app_version_folder(root_dir)? { if let Some((latest_app_dir, _latest_ver)) = shared::get_latest_app_version_folder(&root_dir)? {
fs::rename(latest_app_dir, &current_dir)?; fs::rename(latest_app_dir, &current_dir)?;
} }
} }
info!("Removing old shortcuts..."); info!("Removing old shortcuts...");
win::remove_all_shortcuts_for_root_dir(root_dir); win::remove_all_shortcuts_for_root_dir(&root_dir);
let mut modified_app = app.clone(); // reset current manifest shortcuts, so when the new manifest is being read
modified_app.shortcut_locations = "".to_string(); // reset, so we install new shortcuts // new shortcuts will be force-created
let modified_app = locator.clone_self_with_blank_shortcuts();
info!("Applying latest full package..."); info!("Applying latest full package...");
let buf = Path::new(&package.file_path).to_path_buf(); let buf = Path::new(&package.file_path).to_path_buf();
super::apply(root_dir, &modified_app, false, OperationWait::NoWait, Some(&buf), None, false)?; super::apply(&modified_app, false, OperationWait::NoWait, Some(&buf), None, false)?;
info!("Removing old app-* folders..."); info!("Removing old app-* folders...");
shared::delete_app_prefixed_folders(root_dir)?; shared::delete_app_prefixed_folders(&root_dir)?;
let _ = remove_dir_all::remove_dir_all(root_dir.join("staging")); let _ = remove_dir_all::remove_dir_all(root_dir.join("staging"));
Ok(()) let new_locator = locator.clone_self_with_new_manifest(&bundle_manifest);
Ok(new_locator)
} }

View File

@@ -1,20 +1,25 @@
use crate::shared::{self, bundle::Manifest}; use crate::shared::{self};
use velopack::{locator::VelopackLocator};
use crate::windows; use crate::windows;
use anyhow::Result; use anyhow::Result;
use std::fs::File; use std::fs::File;
use std::path::PathBuf;
pub fn uninstall(root_path: &PathBuf, app: &Manifest, delete_self: bool) -> Result<()> { pub fn uninstall(locator: &VelopackLocator, delete_self: bool) -> Result<()> {
info!("Command: Uninstall"); info!("Command: Uninstall");
let root_path = locator.get_root_dir();
fn _uninstall_impl(app: &Manifest, root_path: &PathBuf) -> bool { fn _uninstall_impl(locator: &VelopackLocator) -> bool {
let root_path = locator.get_root_dir();
// the real app could be running at the moment // the real app could be running at the moment
let _ = shared::force_stop_package(&root_path); let _ = shared::force_stop_package(&root_path);
let mut finished_with_errors = false; let mut finished_with_errors = false;
// run uninstall hook // run uninstall hook
windows::run_hook(&app, root_path, "--veloapp-uninstall", 60); windows::run_hook(&locator, "--veloapp-uninstall", 60);
// remove all shortcuts pointing to the app // remove all shortcuts pointing to the app
windows::remove_all_shortcuts_for_root_dir(&root_path); windows::remove_all_shortcuts_for_root_dir(&root_path);
@@ -24,8 +29,8 @@ pub fn uninstall(root_path: &PathBuf, app: &Manifest, delete_self: bool) -> Resu
error!("Unable to remove directory, some files may be in use ({}).", e); error!("Unable to remove directory, some files may be in use ({}).", e);
finished_with_errors = true; finished_with_errors = true;
} }
if let Err(e) = app.remove_uninstall_entry() { if let Err(e) = windows::registry::remove_uninstall_entry(&locator) {
error!("Unable to remove uninstall registry entry ({}).", e); error!("Unable to remove uninstall registry entry ({}).", e);
// finished_with_errors = true; // finished_with_errors = true;
} }
@@ -35,14 +40,15 @@ pub fn uninstall(root_path: &PathBuf, app: &Manifest, delete_self: bool) -> Resu
// if it returns true, it was a success. // if it returns true, it was a success.
// if it returns false, it was completed with errors which the user should be notified of. // if it returns false, it was completed with errors which the user should be notified of.
let result = _uninstall_impl(&app, &root_path); let result = _uninstall_impl(&locator);
let app_title = locator.get_manifest_title();
if result { if result {
info!("Finished successfully."); info!("Finished successfully.");
shared::dialogs::show_info(format!("{} Uninstall", app.title).as_str(), None, "The application was successfully uninstalled."); shared::dialogs::show_info(format!("{} Uninstall", app_title).as_str(), None, "The application was successfully uninstalled.");
} else { } else {
error!("Finished with errors."); error!("Finished with errors.");
shared::dialogs::show_uninstall_complete_with_errors_dialog(&app, None); shared::dialogs::show_uninstall_complete_with_errors_dialog(&app_title, None);
} }
let dead_path = root_path.join(".dead"); let dead_path = root_path.join(".dead");

View File

@@ -4,7 +4,7 @@ pub mod shared;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub mod windows; pub mod windows;
pub use shared::{bundle, dialogs}; pub use shared::{dialogs};
#[macro_use] #[macro_use]
extern crate log; extern crate log;

View File

@@ -7,31 +7,9 @@ pub fn trace_logger() {
TermLogger::init(LevelFilter::Trace, get_config(None), TerminalMode::Mixed, ColorChoice::Never).unwrap(); TermLogger::init(LevelFilter::Trace, get_config(None), TerminalMode::Mixed, ColorChoice::Never).unwrap();
} }
pub fn default_log_location() -> PathBuf { pub fn setup_logging(process_name: &str, file: Option<&PathBuf>, console: bool, verbose: bool) -> Result<()> {
#[cfg(target_os = "windows")]
{
let mut my_dir = std::env::current_exe().unwrap();
my_dir.pop();
return my_dir.join("Velopack.log");
}
#[cfg(target_os = "linux")]
{
return std::path::Path::new("/tmp/velopack.log").to_path_buf();
}
#[cfg(target_os = "macos")]
{
#[allow(deprecated)]
let mut user_home = std::env::home_dir().expect("Could not locate user home directory via $HOME or /etc/passwd");
user_home.push("Library");
user_home.push("Logs");
user_home.push("velopack.log");
return user_home;
}
}
pub fn setup_logging(process_name: &str, file: Option<&PathBuf>, console: bool, verbose: bool, nocolor: bool) -> Result<()> {
let mut loggers: Vec<Box<dyn SharedLogger>> = Vec::new(); let mut loggers: Vec<Box<dyn SharedLogger>> = Vec::new();
let color_choice = if nocolor { ColorChoice::Never } else { ColorChoice::Auto }; let color_choice = ColorChoice::Never;
if console { if console {
let console_level = if verbose { LevelFilter::Debug } else { LevelFilter::Info }; let console_level = if verbose { LevelFilter::Debug } else { LevelFilter::Info };
loggers.push(TermLogger::new(console_level, get_config(None), TerminalMode::Mixed, color_choice)); loggers.push(TermLogger::new(console_level, get_config(None), TerminalMode::Mixed, color_choice));

View File

@@ -4,13 +4,59 @@
#[macro_use] #[macro_use]
extern crate log; extern crate log;
use anyhow::Result; use anyhow::{bail, Result};
use clap::{arg, value_parser, Command}; use clap::{arg, value_parser, Command};
use memmap2::Mmap;
use std::cell::RefCell;
use std::fs::File;
use std::io::Cursor;
use std::rc::Rc;
use std::{env, path::PathBuf}; use std::{env, path::PathBuf};
use velopack_bins::*; use velopack_bins::*;
#[used]
#[no_mangle]
static BUNDLE_PLACEHOLDER: [u8; 48] = [
0, 0, 0, 0, 0, 0, 0, 0, // 8 bytes for package offset
0, 0, 0, 0, 0, 0, 0, 0, // 8 bytes for package length
0x94, 0xf0, 0xb1, 0x7b, 0x68, 0x93, 0xe0, 0x29, // 32 bytes for bundle signature
0x37, 0xeb, 0x34, 0xef, 0x53, 0xaa, 0xe7, 0xd4, //
0x2b, 0x54, 0xf5, 0x70, 0x7e, 0xf5, 0xd6, 0xf5, //
0x78, 0x54, 0x98, 0x3e, 0x5e, 0x94, 0xed, 0x7d, //
];
#[used]
#[inline(never)]
pub fn header_offset_and_length() -> (i64, i64) {
use core::ptr;
// Perform volatile reads to avoid optimization issues
// TODO: refactor to use little-endian, also need to update the writer in dotnet
unsafe {
let offset = i64::from_ne_bytes([
ptr::read_volatile(&BUNDLE_PLACEHOLDER[0]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[1]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[2]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[3]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[4]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[5]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[6]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[7]),
]);
let length = i64::from_ne_bytes([
ptr::read_volatile(&BUNDLE_PLACEHOLDER[8]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[9]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[10]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[11]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[12]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[13]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[14]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[15]),
]);
(offset, length)
}
}
fn main() -> Result<()> { fn main() -> Result<()> {
#[cfg(windows)]
windows::mitigate::pre_main_sideload_mitigation(); windows::mitigate::pre_main_sideload_mitigation();
#[rustfmt::skip] #[rustfmt::skip]
@@ -20,30 +66,42 @@ fn main() -> Result<()> {
.arg(arg!(-v --verbose "Print debug messages to console")) .arg(arg!(-v --verbose "Print debug messages to console"))
.arg(arg!(-l --log <FILE> "Enable file logging and set location").required(false).value_parser(value_parser!(PathBuf))) .arg(arg!(-l --log <FILE> "Enable file logging and set location").required(false).value_parser(value_parser!(PathBuf)))
.arg(arg!(-t --installto <DIR> "Installation directory to install the application").required(false).value_parser(value_parser!(PathBuf))) .arg(arg!(-t --installto <DIR> "Installation directory to install the application").required(false).value_parser(value_parser!(PathBuf)))
.arg(arg!(--nocolor "Disable colored output").hide(true)); .arg(arg!([EXE_ARGS] "Arguments to pass to the started executable. Must be preceded by '--'.").required(false).last(true).num_args(0..));
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
arg_config = arg_config arg_config = arg_config
.arg(arg!(-d --debug <FILE> "Debug mode, install from a nupkg file").required(false).value_parser(value_parser!(PathBuf))); .arg(arg!(-d --debug <FILE> "Debug mode, install from a nupkg file").required(false).value_parser(value_parser!(PathBuf)));
} }
let matches = arg_config.get_matches(); let res = run_inner(arg_config);
if let Err(e) = &res {
error!("An error has occurred: {}", e);
dialogs::show_error("Setup Error", None, format!("An error has occurred: {}", e).as_str());
}
Ok(())
}
fn run_inner(arg_config: Command) -> Result<()>
{
let matches = arg_config.try_get_matches()?;
let silent = matches.get_flag("silent"); let silent = matches.get_flag("silent");
let verbose = matches.get_flag("verbose"); let verbose = matches.get_flag("verbose");
let debug = matches.get_one::<PathBuf>("debug"); let debug = matches.get_one::<PathBuf>("debug");
let logfile = matches.get_one::<PathBuf>("log"); let logfile = matches.get_one::<PathBuf>("log");
let installto = matches.get_one::<PathBuf>("installto"); let install_to = matches.get_one::<PathBuf>("installto");
let nocolor = matches.get_flag("nocolor"); let exe_args: Option<Vec<&str>> = matches.get_many::<String>("EXE_ARGS").map(|v| v.map(|f| f.as_str()).collect());
shared::dialogs::set_silent(silent); dialogs::set_silent(silent);
logging::setup_logging("setup", logfile, true, verbose, nocolor)?; logging::setup_logging("setup", logfile, true, verbose)?;
info!("Starting Velopack Setup ({})", env!("NGBV_VERSION")); info!("Starting Velopack Setup ({})", env!("NGBV_VERSION"));
info!(" Location: {:?}", std::env::current_exe()?); info!(" Location: {:?}", env::current_exe()?);
info!(" Silent: {}", silent); info!(" Silent: {}", silent);
info!(" Verbose: {}", verbose); info!(" Verbose: {}", verbose);
info!(" Log: {:?}", logfile); info!(" Log: {:?}", logfile);
info!(" Install To: {:?}", installto); info!(" Install To: {:?}", install_to);
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
info!(" Debug: {:?}", debug); info!(" Debug: {:?}", debug);
} }
@@ -53,12 +111,40 @@ fn main() -> Result<()> {
containing_dir.pop(); containing_dir.pop();
env::set_current_dir(containing_dir)?; env::set_current_dir(containing_dir)?;
let res = commands::install(debug, installto); // load the bundle which is embedded or if missing from the debug nupkg path
if let Err(e) = &res { let osinfo = os_info::get();
error!("An error has occurred: {}", e); let osarch = shared::runtime_arch::RuntimeArch::from_current_system();
dialogs::show_error("Setup Error", None, format!("An error has occurred: {}", e).as_str()); info!("OS: {osinfo}, Arch={osarch:#?}");
if !windows::is_windows_7_sp1_or_greater() {
bail!("This installer requires Windows 7 SPA1 or later and cannot run.");
} }
res?; // in debug mode only, allow a nupkg to be passed in as the first argument
Ok(()) if cfg!(debug_assertions) {
if let Some(pkg) = debug {
info!("Loading bundle from DEBUG nupkg file {:?}...", pkg);
let mut bundle = velopack::bundle::load_bundle_from_file(pkg)?;
commands::install(&mut bundle, install_to, exe_args)?;
return Ok(())
}
}
info!("Reading bundle header...");
let (offset, length) = header_offset_and_length();
info!("Bundle offset = {}, length = {}", offset, length);
// try to load the bundle from embedded zip
if offset > 0 && length > 0 {
info!("Loading bundle from embedded zip...");
let file = File::open(env::current_exe()?)?;
let mmap = unsafe { Mmap::map(&file)? };
let zip_range: &[u8] = &mmap[offset as usize..(offset + length) as usize];
let mut bundle = velopack::bundle::load_bundle_from_memory(&zip_range)?;
commands::install(&mut bundle, install_to, exe_args)?;
return Ok(())
}
bail!("Could not find embedded zip file. Please contact the application author.");
} }

View File

@@ -1,634 +0,0 @@
use anyhow::{anyhow, bail, Result};
use regex::Regex;
use semver::Version;
use std::{
cell::RefCell,
fs::{self, File},
io::{Cursor, Read, Seek, Write},
path::{Path, PathBuf},
rc::Rc,
};
use xml::reader::{EventReader, XmlEvent};
use zip::ZipArchive;
#[cfg(target_os = "macos")]
use std::os::unix::fs::PermissionsExt;
#[cfg(target_os = "windows")]
use chrono::{Datelike, Local as DateTime};
#[cfg(target_os = "windows")]
use memmap2::Mmap;
#[cfg(target_os = "windows")]
use normpath::PathExt;
#[cfg(target_os = "windows")]
use winsafe::{self as w, co, prelude::*};
pub trait ReadSeek: Read + Seek {}
impl<T: Read + Seek> ReadSeek for T {}
#[cfg(target_os = "windows")]
#[used]
#[no_mangle]
static BUNDLE_PLACEHOLDER: [u8; 48] = [
0, 0, 0, 0, 0, 0, 0, 0, // 8 bytes for package offset
0, 0, 0, 0, 0, 0, 0, 0, // 8 bytes for package length
0x94, 0xf0, 0xb1, 0x7b, 0x68, 0x93, 0xe0, 0x29, // 32 bytes for bundle signature
0x37, 0xeb, 0x34, 0xef, 0x53, 0xaa, 0xe7, 0xd4, //
0x2b, 0x54, 0xf5, 0x70, 0x7e, 0xf5, 0xd6, 0xf5, //
0x78, 0x54, 0x98, 0x3e, 0x5e, 0x94, 0xed, 0x7d, //
];
#[cfg(target_os = "windows")]
#[inline(never)]
pub fn header_offset_and_length() -> (i64, i64) {
use core::ptr;
// Perform volatile reads to avoid optimization issues
// TODO: refactor to use little-endian, also need to update the writer in dotnet
unsafe {
let offset = i64::from_ne_bytes([
ptr::read_volatile(&BUNDLE_PLACEHOLDER[0]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[1]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[2]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[3]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[4]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[5]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[6]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[7]),
]);
let length = i64::from_ne_bytes([
ptr::read_volatile(&BUNDLE_PLACEHOLDER[8]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[9]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[10]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[11]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[12]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[13]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[14]),
ptr::read_volatile(&BUNDLE_PLACEHOLDER[15]),
]);
(offset, length)
}
}
#[cfg(target_os = "windows")]
pub fn load_bundle_from_mmap<'a>(mmap: &'a Mmap, debug_pkg: Option<&PathBuf>) -> Result<BundleInfo<'a>> {
info!("Reading bundle header...");
let (offset, length) = header_offset_and_length();
info!("Bundle offset = {}, length = {}", offset, length);
let zip_range: &'a [u8] = &mmap[offset as usize..(offset + length) as usize];
// try to load the bundle from embedded zip
if offset > 0 && length > 0 {
info!("Loading bundle from embedded zip...");
let cursor: Box<dyn ReadSeek> = Box::new(Cursor::new(zip_range));
let zip = ZipArchive::new(cursor).map_err(|e| anyhow::Error::new(e))?;
return Ok(BundleInfo { zip: Rc::new(RefCell::new(zip)), zip_from_file: false, zip_range: Some(zip_range), file_path: None });
}
// in debug mode only, allow a nupkg to be passed in as the first argument
if cfg!(debug_assertions) {
if let Some(pkg) = debug_pkg {
info!("Loading bundle from debug nupkg file...");
return load_bundle_from_file(pkg);
}
}
bail!("Could not find embedded zip file. Please contact the application author.");
}
#[derive(Clone)]
pub struct BundleInfo<'a> {
zip: Rc<RefCell<ZipArchive<Box<dyn ReadSeek + 'a>>>>,
zip_from_file: bool,
zip_range: Option<&'a [u8]>,
file_path: Option<PathBuf>,
}
pub fn load_bundle_from_file<'a, P: AsRef<Path>>(file_name: P) -> Result<BundleInfo<'a>> {
let file_name = file_name.as_ref();
debug!("Loading bundle from file '{}'...", file_name.to_string_lossy());
let file = super::retry_io(|| File::open(&file_name))?;
let cursor: Box<dyn ReadSeek> = Box::new(file);
let zip = ZipArchive::new(cursor)?;
return Ok(BundleInfo { zip: Rc::new(RefCell::new(zip)), zip_from_file: true, file_path: Some(file_name.to_owned()), zip_range: None });
}
impl BundleInfo<'_> {
pub fn calculate_size(&self) -> (u64, u64) {
let mut total_uncompressed_size = 0u64;
let mut total_compressed_size = 0u64;
let mut archive = self.zip.borrow_mut();
for i in 0..archive.len() {
let file = archive.by_index(i);
if file.is_ok() {
let file = file.unwrap();
total_uncompressed_size += file.size();
total_compressed_size += file.compressed_size();
}
}
(total_compressed_size, total_uncompressed_size)
}
pub fn get_splash_bytes(&self) -> Option<Vec<u8>> {
let splash_idx = self.find_zip_file(|name| name.contains("splashimage"));
if splash_idx.is_none() {
warn!("Could not find splash image in bundle.");
return None;
}
let mut archive = self.zip.borrow_mut();
let sf = archive.by_index(splash_idx.unwrap());
if sf.is_err() {
warn!("Could not find splash image in bundle.");
return None;
}
let res: Result<Vec<u8>, _> = sf.unwrap().bytes().collect();
if res.is_err() {
warn!("Could not find splash image in bundle.");
return None;
}
let bytes = res.unwrap();
if bytes.is_empty() {
warn!("Could not find splash image in bundle.");
return None;
}
Some(bytes)
}
pub fn find_zip_file<F>(&self, predicate: F) -> Option<usize>
where
F: Fn(&str) -> bool,
{
let mut archive = self.zip.borrow_mut();
for i in 0..archive.len() {
if let Ok(file) = archive.by_index(i) {
let name = file.name();
if predicate(name) {
return Some(i);
}
}
}
None
}
pub fn extract_zip_idx_to_path<T: AsRef<Path>>(&self, index: usize, path: T) -> Result<()> {
let path = path.as_ref();
debug!("Extracting zip file to path: {}", path.to_string_lossy());
let p = PathBuf::from(path);
let parent = p.parent().unwrap();
if !parent.exists() {
debug!("Creating parent directory: {:?}", parent);
super::retry_io(|| fs::create_dir_all(parent))?;
}
let mut archive = self.zip.borrow_mut();
let mut file = archive.by_index(index)?;
let mut outfile = super::retry_io(|| File::create(path))?;
let mut buffer = [0; 64000]; // Use a 64KB buffer; good balance for large/small files.
debug!("Writing normal file to disk with 64k buffer: {:?}", path);
loop {
let len = file.read(&mut buffer)?;
if len == 0 {
break; // End of file
}
outfile.write_all(&buffer[..len])?;
}
Ok(())
}
pub fn extract_zip_predicate_to_path<F, T: AsRef<Path>>(&self, predicate: F, path: T) -> Result<usize>
where
F: Fn(&str) -> bool,
{
let idx = self.find_zip_file(predicate);
if idx.is_none() {
bail!("Could not find file in bundle.");
}
let idx = idx.unwrap();
self.extract_zip_idx_to_path(idx, path)?;
Ok(idx)
}
#[cfg(not(target_os = "linux"))]
fn create_symlink(link_path: &PathBuf, target_path: &PathBuf) -> Result<()> {
#[cfg(target_os = "windows")]
{
let absolute_path = link_path.parent().unwrap().join(&target_path);
trace!(
"Creating symlink '{}' -> '{}', target isfile={}, isdir={}, relative={}",
link_path.to_string_lossy(),
absolute_path.to_string_lossy(),
absolute_path.is_file(),
absolute_path.is_dir(),
target_path.to_string_lossy()
);
if absolute_path.is_file() {
std::os::windows::fs::symlink_file(target_path, link_path)?;
} else if absolute_path.is_dir() {
std::os::windows::fs::symlink_dir(target_path, link_path)?;
} else {
bail!("Could not create symlink: target is not a file or directory.")
}
}
#[cfg(not(target_os = "windows"))]
{
std::os::unix::fs::symlink(target_path, link_path)?;
}
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub fn extract_lib_contents_to_path<P: AsRef<Path>, F: Fn(i16)>(&self, current_path: P, progress: F) -> Result<()> {
let current_path = current_path.as_ref();
let files = self.get_file_names()?;
let num_files = files.len();
info!("Extracting {} app files to '{}'...", num_files, current_path.to_string_lossy());
let re = Regex::new(r"lib[\\\/][^\\\/]*[\\\/]").unwrap();
let stub_regex = Regex::new("_ExecutionStub.exe$").unwrap();
let symlink_regex = Regex::new(".__symlink$").unwrap();
let updater_idx = self.find_zip_file(|name| name.ends_with("Squirrel.exe"));
// for legacy support, we still extract the nuspec file to the current dir.
// in newer versions, the nuspec is in the current dir in the package itself.
#[cfg(target_os = "windows")]
{
let nuspec_path = current_path.join("sq.version");
let _ = self
.extract_zip_predicate_to_path(|name| name.ends_with(".nuspec"), nuspec_path)
.map_err(|_| anyhow!("This package is missing a nuspec manifest."))?;
}
// we extract the symlinks after, because the target must exist.
let mut symlinks: Vec<(usize, PathBuf)> = Vec::new();
for (i, key) in files.iter().enumerate() {
if Some(i) == updater_idx || !re.is_match(key) || key.ends_with("/") || key.ends_with("\\") {
debug!(" {} Skipped '{}'", i, key);
continue;
}
let file_path_in_zip = re.replace(key, "").to_string();
let file_path_on_disk = Path::new(&current_path).join(&file_path_in_zip);
if symlink_regex.is_match(&file_path_in_zip) {
let sym_key = symlink_regex.replace(&file_path_in_zip, "").to_string();
let file_path_on_disk = Path::new(&current_path).join(&sym_key);
symlinks.push((i, file_path_on_disk));
continue;
}
if stub_regex.is_match(&file_path_in_zip) {
// let stub_key = stub_regex.replace(&file_path_in_zip, ".exe").to_string();
// file_path_on_disk = root_path.join(&stub_key);
debug!(" {} Skipped Stub (obsolete) '{}'", i, key);
continue;
}
// on windows, the zip paths are / and should be \ instead
#[cfg(target_os = "windows")]
let file_path_on_disk = file_path_on_disk.normalize_virtually()?;
#[cfg(target_os = "windows")]
let file_path_on_disk = file_path_on_disk.as_path();
debug!(" {} Extracting '{}' to '{}'", i, key, file_path_on_disk.to_string_lossy());
self.extract_zip_idx_to_path(i, &file_path_on_disk)?;
// on macos, we need to chmod +x the executable files
#[cfg(target_os = "macos")]
{
if let Ok(true) = super::macho::is_macho_image(&file_path_on_disk) {
if let Err(e) = std::fs::set_permissions(&file_path_on_disk, std::fs::Permissions::from_mode(0o755)) {
warn!("Failed to set executable permissions on '{}': {}", file_path_on_disk.to_string_lossy(), e);
} else {
info!(" {} Set executable permissions on '{}'", i, file_path_on_disk.to_string_lossy());
}
}
}
progress(((i as f32 / num_files as f32) * 100.0) as i16);
}
// we extract the symlinks after, because the target must exist.
for (i, link_path) in symlinks {
let mut archive = self.zip.borrow_mut();
let mut file = archive.by_index(i)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
info!(" {} Creating symlink '{}' -> '{}'", i, link_path.to_string_lossy(), contents);
let contents = contents.trim_end_matches('/');
#[cfg(target_os = "windows")]
let contents = contents.replace("/", "\\");
let contents = PathBuf::from(contents);
let parent = link_path.parent().unwrap();
if !parent.exists() {
debug!("Creating parent directory: {:?}", parent);
super::retry_io(|| fs::create_dir_all(parent))?;
}
super::retry_io(|| Self::create_symlink(&link_path, &contents))?;
}
Ok(())
}
pub fn read_manifest(&self) -> Result<Manifest> {
let nuspec_idx = self
.find_zip_file(|name| name.ends_with(".nuspec"))
.ok_or_else(|| anyhow!("This installer is missing a package manifest (.nuspec). Please contact the application author."))?;
let mut contents = String::new();
let mut archive = self.zip.borrow_mut();
archive.by_index(nuspec_idx)?.read_to_string(&mut contents)?;
let app = read_manifest_from_string(&contents)?;
Ok(app)
}
pub fn copy_bundle_to_file<T: AsRef<str>>(&self, nupkg_path: T) -> Result<()> {
let nupkg_path = nupkg_path.as_ref();
if self.zip_from_file {
super::retry_io(|| fs::copy(self.file_path.clone().unwrap(), nupkg_path))?;
} else {
super::retry_io(|| fs::write(nupkg_path, self.zip_range.unwrap()))?;
}
Ok(())
}
pub fn len(&self) -> usize {
let archive = self.zip.borrow();
archive.len()
}
pub fn get_file_names(&self) -> Result<Vec<String>> {
let mut files: Vec<String> = Vec::new();
let mut archive = self.zip.borrow_mut();
for i in 0..archive.len() {
let file = archive.by_index(i)?;
let key = file.enclosed_name().ok_or_else(|| {
anyhow!(
"Could not extract file safely ({}). Ensure no paths in archive are absolute or point to a path outside the archive.",
file.name()
)
})?;
files.push(key.to_string_lossy().to_string());
}
Ok(files)
}
}
#[derive(Debug, derivative::Derivative, Clone)]
#[derivative(Default)]
pub struct Manifest {
pub id: String,
#[derivative(Default(value = "Version::new(0, 0, 0)"))]
pub version: Version,
pub title: String,
pub authors: String,
pub description: String,
pub machine_architecture: String,
pub runtime_dependencies: String,
pub main_exe: String,
pub os: String,
pub os_min_version: String,
pub channel: String,
pub shortcut_locations: String,
pub shortcut_amuid: String,
}
#[cfg(target_os = "windows")]
impl Manifest {
const UNINST_STR: &'static str = "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall";
pub fn get_update_path(&self, root_path: &PathBuf) -> String {
root_path.join("Update.exe").to_string_lossy().to_string()
}
pub fn get_main_exe_path(&self, root_path: &PathBuf) -> String {
root_path.join("current").join(&self.main_exe).to_string_lossy().to_string()
}
pub fn get_packages_path(&self, root_path: &PathBuf) -> String {
root_path.join("packages").to_string_lossy().to_string()
}
pub fn get_current_path(&self, root_path: &PathBuf) -> String {
root_path.join("current").to_string_lossy().to_string()
}
pub fn get_nuspec_path(&self, root_path: &PathBuf) -> String {
root_path.join("current").join("sq.version").to_string_lossy().to_string()
}
pub fn get_target_nupkg_path(&self, root_path: &PathBuf) -> String {
root_path.join("packages").join(format!("{}-{}-full.nupkg", self.id, self.version)).to_string_lossy().to_string()
}
pub fn write_uninstall_entry(&self, root_path: &PathBuf) -> Result<()> {
info!("Writing uninstall registry key...");
let root_path_str = root_path.to_string_lossy().to_string();
let main_exe_path = self.get_main_exe_path(root_path);
let updater_path = self.get_update_path(root_path);
let folder_size = fs_extra::dir::get_size(&root_path).unwrap();
let sver = &self.version;
let sver_str = format!("{}.{}.{}", sver.major, sver.minor, sver.patch);
let now = DateTime::now();
let formatted_date = format!("{}{:02}{:02}", now.year(), now.month(), now.day());
let uninstall_cmd = format!("\"{}\" --uninstall", updater_path);
let uninstall_quiet = format!("\"{}\" --uninstall --silent", updater_path);
let reg_uninstall =
w::HKEY::CURRENT_USER.RegCreateKeyEx(Self::UNINST_STR, None, co::REG_OPTION::NoValue, co::KEY::CREATE_SUB_KEY, None)?.0;
let reg_app = reg_uninstall.RegCreateKeyEx(&self.id, None, co::REG_OPTION::NoValue, co::KEY::ALL_ACCESS, None)?.0;
reg_app.RegSetKeyValue(None, Some("DisplayIcon"), w::RegistryValue::Sz(main_exe_path))?;
reg_app.RegSetKeyValue(None, Some("DisplayName"), w::RegistryValue::Sz(self.title.to_owned()))?;
reg_app.RegSetKeyValue(None, Some("DisplayVersion"), w::RegistryValue::Sz(sver_str))?;
reg_app.RegSetKeyValue(None, Some("InstallDate"), w::RegistryValue::Sz(formatted_date))?;
reg_app.RegSetKeyValue(None, Some("InstallLocation"), w::RegistryValue::Sz(root_path_str.to_owned()))?;
reg_app.RegSetKeyValue(None, Some("Publisher"), w::RegistryValue::Sz(self.authors.to_owned()))?;
reg_app.RegSetKeyValue(None, Some("QuietUninstallString"), w::RegistryValue::Sz(uninstall_quiet))?;
reg_app.RegSetKeyValue(None, Some("UninstallString"), w::RegistryValue::Sz(uninstall_cmd))?;
reg_app.RegSetKeyValue(None, Some("EstimatedSize"), w::RegistryValue::Dword((folder_size / 1024).try_into()?))?;
reg_app.RegSetKeyValue(None, Some("NoModify"), w::RegistryValue::Dword(1))?;
reg_app.RegSetKeyValue(None, Some("NoRepair"), w::RegistryValue::Dword(1))?;
reg_app.RegSetKeyValue(None, Some("Language"), w::RegistryValue::Dword(0x0409))?;
Ok(())
}
pub fn remove_uninstall_entry(&self) -> Result<()> {
info!("Removing uninstall registry keys...");
let reg_uninstall =
w::HKEY::CURRENT_USER.RegCreateKeyEx(Self::UNINST_STR, None, co::REG_OPTION::NoValue, co::KEY::CREATE_SUB_KEY, None)?.0;
reg_uninstall.RegDeleteKey(&self.id)?;
Ok(())
}
}
pub fn read_manifest_from_string(xml: &str) -> Result<Manifest> {
let mut obj: Manifest = Default::default();
let cursor = Cursor::new(xml);
let parser = EventReader::new(cursor);
let mut vec: Vec<String> = Vec::new();
for e in parser {
match e {
Ok(XmlEvent::StartElement { name, .. }) => {
vec.push(name.local_name);
}
Ok(XmlEvent::Characters(text)) => {
if vec.is_empty() {
continue;
}
let el_name = vec.last().unwrap();
if el_name == "id" {
obj.id = text;
} else if el_name == "version" {
obj.version = Version::parse(&text)?;
} else if el_name == "title" {
obj.title = text;
} else if el_name == "authors" {
obj.authors = text;
} else if el_name == "description" {
obj.description = text;
} else if el_name == "machineArchitecture" {
obj.machine_architecture = text;
} else if el_name == "runtimeDependencies" {
obj.runtime_dependencies = text;
} else if el_name == "mainExe" {
obj.main_exe = text;
} else if el_name == "os" {
obj.os = text;
} else if el_name == "osMinVersion" {
obj.os_min_version = text;
} else if el_name == "channel" {
obj.channel = text;
} else if el_name == "shortcutLocations" {
obj.shortcut_locations = text;
} else if el_name == "shortcutAmuid" {
obj.shortcut_amuid = text;
}
}
Ok(XmlEvent::EndElement { .. }) => {
vec.pop();
}
Err(e) => {
error!("Error: {e}");
break;
}
// There's more: https://docs.rs/xml-rs/latest/xml/reader/enum.XmlEvent.html
_ => {}
}
}
if obj.id.is_empty() {
bail!("Missing 'id' in package manifest. Please contact the application author.");
}
if obj.version == Version::new(0, 0, 0) {
bail!("Missing 'version' in package manifest. Please contact the application author.");
}
#[cfg(target_os = "windows")]
if obj.main_exe.is_empty() {
bail!("Missing 'mainExe' in package manifest. Please contact the application author.");
}
if obj.title.is_empty() {
obj.title = obj.id.clone();
}
Ok(obj)
}
#[derive(Debug, Clone, derivative::Derivative)]
#[derivative(Default)]
pub struct EntryNameInfo {
pub name: String,
#[derivative(Default(value = "Version::new(0, 0, 0)"))]
pub version: Version,
pub is_delta: bool,
pub file_path: String,
}
impl EntryNameInfo {
pub fn load_manifest(&self) -> Result<Manifest> {
let path = Path::new(&self.file_path).to_path_buf();
let bundle = load_bundle_from_file(&path)?;
bundle.read_manifest()
}
}
lazy_static! {
static ref ENTRY_SUFFIX_FULL: Regex = Regex::new(r"(?i)-full.nupkg$").unwrap();
static ref ENTRY_SUFFIX_DELTA: Regex = Regex::new(r"(?i)-delta.nupkg$").unwrap();
static ref ENTRY_VERSION_START: Regex = Regex::new(r"[\.-](0|[1-9]\d*)\.(0|[1-9]\d*)($|[^\d])").unwrap();
}
pub fn parse_package_file_path(path: PathBuf) -> Option<EntryNameInfo> {
let name = path.file_name()?.to_string_lossy().to_string();
let m = parse_package_file_name(name);
if m.is_some() {
let mut m = m.unwrap();
m.file_path = path.to_string_lossy().to_string();
return Some(m);
}
m
}
fn parse_package_file_name<T: AsRef<str>>(name: T) -> Option<EntryNameInfo> {
let name = name.as_ref();
let full = ENTRY_SUFFIX_FULL.is_match(name);
let delta = ENTRY_SUFFIX_DELTA.is_match(name);
if !full && !delta {
return None;
}
let mut entry = EntryNameInfo::default();
entry.is_delta = delta;
let name_and_ver = if full { ENTRY_SUFFIX_FULL.replace(name, "") } else { ENTRY_SUFFIX_DELTA.replace(name, "") };
let ver_idx = ENTRY_VERSION_START.find(&name_and_ver);
if ver_idx.is_none() {
return None;
}
let ver_idx = ver_idx.unwrap().start();
entry.name = name_and_ver[0..ver_idx].to_string();
let ver_idx = ver_idx + 1;
let version = name_and_ver[ver_idx..].to_string();
let sv = Version::parse(&version);
if sv.is_err() {
return None;
}
entry.version = sv.unwrap();
return Some(entry);
}
#[test]
fn test_parse_package_file_name() {
// test no rid
let entry = parse_package_file_name("Velopack-1.0.0-full.nupkg").unwrap();
assert_eq!(entry.name, "Velopack");
assert_eq!(entry.version, Version::parse("1.0.0").unwrap());
assert_eq!(entry.is_delta, false);
let entry = parse_package_file_name("Velopack-1.0.0-delta.nupkg").unwrap();
assert_eq!(entry.name, "Velopack");
assert_eq!(entry.version, Version::parse("1.0.0").unwrap());
assert_eq!(entry.is_delta, true);
let entry = parse_package_file_name("My.Cool-App-1.1.0-full.nupkg").unwrap();
assert_eq!(entry.name, "My.Cool-App");
assert_eq!(entry.version, Version::parse("1.1.0").unwrap());
assert_eq!(entry.is_delta, false);
// test invalid names
assert!(parse_package_file_name("MyCoolApp-1.2.3-beta1-win7-x64-full.nupkg.zip").is_none());
assert!(parse_package_file_name("MyCoolApp-1.2.3-beta1-win7-x64-full.zip").is_none());
assert!(parse_package_file_name("MyCoolApp-1.2.3.nupkg").is_none());
assert!(parse_package_file_name("MyCoolApp-1.2-full.nupkg").is_none());
}

View File

@@ -43,14 +43,14 @@ pub fn show_ok_cancel(title: &str, header: Option<&str>, body: &str, ok_text: Op
generate_confirm(title, header, body, ok_text, btns, DialogIcon::Warning).map(|dlg_id| dlg_id == DialogResult::Ok).unwrap_or(false) generate_confirm(title, header, body, ok_text, btns, DialogIcon::Warning).map(|dlg_id| dlg_id == DialogResult::Ok).unwrap_or(false)
} }
pub fn ask_user_to_elevate(app_to: &crate::bundle::Manifest) -> Result<()> { pub fn ask_user_to_elevate(app_title: &str, new_version: &str) -> Result<()> {
if get_silent() { if get_silent() {
bail!("Not allowed to ask for elevated permissions because --silent flag is set."); bail!("Not allowed to ask for elevated permissions because --silent flag is set.");
} }
let title = format!("{} Update", app_to.title); let title = format!("{} Update", app_title);
let body = let body =
format!("{} would like to update to version {}, but requires elevated permissions to do so. Would you like to proceed?", app_to.title, app_to.version); format!("{} would like to update to version {}, but requires elevated permissions to do so. Would you like to proceed?", app_title, new_version);
info!("Showing user elevation prompt?"); info!("Showing user elevation prompt?");
if show_ok_cancel(title.as_str(), None, body.as_str(), Some("Install Update")) { if show_ok_cancel(title.as_str(), None, body.as_str(), Some("Install Update")) {

View File

@@ -1,7 +1,9 @@
use super::{bundle::Manifest, dialogs_common::*, dialogs_const::*}; use super::{dialogs_common::*, dialogs_const::*};
use velopack::bundle::Manifest;
use anyhow::Result; use anyhow::Result;
use std::path::PathBuf; use std::path::PathBuf;
use winsafe::{self as w, co, prelude::*, WString}; use winsafe::{self as w, co, prelude::*, WString};
use velopack::locator::{auto_locate_app_manifest, LocationContext};
pub fn show_restart_required(app: &Manifest) { pub fn show_restart_required(app: &Manifest) {
show_warn( show_warn(
@@ -32,7 +34,7 @@ pub fn show_update_missing_dependencies_dialog(
"{} {to} has missing dependencies which need to be installed: {}, would you like to continue?", "{} {to} has missing dependencies which need to be installed: {}, would you like to continue?",
app.title, depedency_string app.title, depedency_string
) )
.as_str(), .as_str(),
Some("Install & Update"), Some("Install & Update"),
) )
} }
@@ -51,13 +53,13 @@ pub fn show_setup_missing_dependencies_dialog(app: &Manifest, depedency_string:
) )
} }
pub fn show_uninstall_complete_with_errors_dialog(app: &Manifest, log_path: Option<&PathBuf>) { pub fn show_uninstall_complete_with_errors_dialog(app_title: &str, log_path: Option<&PathBuf>) {
if get_silent() { if get_silent() {
return; return;
} }
let mut setup_name = WString::from_str(format!("{} Uninstall", app.title)); let mut setup_name = WString::from_str(format!("{} Uninstall", app_title));
let mut instruction = WString::from_str(format!("{} uninstall has completed with errors.", app.title)); let mut instruction = WString::from_str(format!("{} uninstall has completed with errors.", app_title));
let mut content = WString::from_str( let mut content = WString::from_str(
"There may be left-over files or directories on your system. You can attempt to remove these manually or re-install the application and try again.", "There may be left-over files or directories on your system. You can attempt to remove these manually or re-install the application and try again.",
); );
@@ -84,7 +86,7 @@ pub fn show_uninstall_complete_with_errors_dialog(app: &Manifest, log_path: Opti
let _ = w::TaskDialogIndirect(&config, None); let _ = w::TaskDialogIndirect(&config, None);
} }
pub fn show_processes_locking_folder_dialog(app: &Manifest, process_names: &str) -> DialogResult { pub fn show_processes_locking_folder_dialog(app_title: &str, app_version: &str, process_names: &str) -> DialogResult {
if get_silent() { if get_silent() {
return DialogResult::Cancel; return DialogResult::Cancel;
} }
@@ -92,13 +94,13 @@ pub fn show_processes_locking_folder_dialog(app: &Manifest, process_names: &str)
let mut config: w::TASKDIALOGCONFIG = Default::default(); let mut config: w::TASKDIALOGCONFIG = Default::default();
config.set_pszMainIcon(w::IconIdTdicon::Tdicon(co::TD_ICON::INFORMATION)); config.set_pszMainIcon(w::IconIdTdicon::Tdicon(co::TD_ICON::INFORMATION));
let mut update_name = WString::from_str(format!("{} Update {}", app.title, app.version)); let mut update_name = WString::from_str(format!("{} Update {}", app_title, app_version));
let mut instruction = WString::from_str(format!("{} Update", app.title)); let mut instruction = WString::from_str(format!("{} Update", app_title));
let mut content = WString::from_str(format!( let mut content = WString::from_str(format!(
"There are programs ({}) preventing the {} update from proceeding. \n\n\ "There are programs ({}) preventing the {} update from proceeding. \n\n\
You can press Continue to have this updater attempt to close them automatically, or if you've closed them yourself press Retry for the updater to check again.", You can press Continue to have this updater attempt to close them automatically, or if you've closed them yourself press Retry for the updater to check again.",
process_names, app.title)); process_names, app_title));
let mut btn_retry_txt = WString::from_str("Retry\nTry again if you've closed the program(s)"); let mut btn_retry_txt = WString::from_str("Retry\nTry again if you've closed the program(s)");
let mut btn_continue_txt = WString::from_str("Continue\nAttempt to close the program(s) automatically"); let mut btn_continue_txt = WString::from_str("Continue\nAttempt to close the program(s) automatically");
@@ -145,19 +147,19 @@ pub fn show_overwrite_repair_dialog(app: &Manifest, root_path: &PathBuf, root_is
let mut btn_cancel_txt = WString::from_str("Cancel\nBackup or save your work first"); let mut btn_cancel_txt = WString::from_str("Cancel\nBackup or save your work first");
// if we can detect the current app version, we call it "Update" or "Downgrade" // if we can detect the current app version, we call it "Update" or "Downgrade"
let possible_update = root_path.join("Update.exe"); let old_app = auto_locate_app_manifest(LocationContext::FromSpecifiedRootDir(root_path.to_owned()));
let old_app = super::detect_manifest_from_update_path(&possible_update).map(|v| v.1).ok(); if let Ok(old) = old_app {
if let Some(old) = old_app { let old_version = old.get_manifest_version();
if old.version < app.version { if old_version < app.version {
instruction = WString::from_str(format!("An older version of {} is installed.", app.title)); instruction = WString::from_str(format!("An older version of {} is installed.", app.title));
content = WString::from_str(format!("Would you like to update from {} to {}?", old.version, app.version)); content = WString::from_str(format!("Would you like to update from {} to {}?", old_version, app.version));
btn_yes_txt = WString::from_str(format!("Update\nTo version {}", app.version)); btn_yes_txt = WString::from_str(format!("Update\nTo version {}", app.version));
config.set_pszMainIcon(w::IconIdTdicon::Tdicon(co::TD_ICON::INFORMATION)); config.set_pszMainIcon(w::IconIdTdicon::Tdicon(co::TD_ICON::INFORMATION));
} else if old.version > app.version { } else if old_version > app.version {
instruction = WString::from_str(format!("A newer version of {} is installed.", app.title)); instruction = WString::from_str(format!("A newer version of {} is installed.", app.title));
content = WString::from_str(format!( content = WString::from_str(format!(
"You already have {} installed. Would you like to downgrade this application to an older version?", "You already have {} installed. Would you like to downgrade this application to an older version?",
old.version old_version
)); ));
btn_yes_txt = WString::from_str(format!("Downgrade\nTo version {}", app.version)); btn_yes_txt = WString::from_str(format!("Downgrade\nTo version {}", app.version));
} }

View File

@@ -1,98 +0,0 @@
use crate::shared;
use anyhow::Result;
use std::fs::File;
use std::io::{Read, Write};
pub fn download_url_to_file<A>(url: &str, file_path: &str, mut progress: A) -> Result<()>
where
A: FnMut(i16),
{
let agent = get_download_agent()?;
let response = agent.get(url).call()?;
let total_size = response.header("Content-Length").and_then(|s| s.parse::<u64>().ok());
let mut file = shared::retry_io(|| File::create(file_path))?;
const CHUNK_SIZE: usize = 2 * 1024 * 1024; // 2MB
let mut downloaded: u64 = 0;
let mut buffer = vec![0; CHUNK_SIZE];
let mut reader = response.into_reader();
let mut last_progress = 0;
while let Ok(size) = reader.read(&mut buffer) {
if size == 0 {
break; // End of stream
}
file.write_all(&buffer[..size])?;
downloaded += size as u64;
if total_size.is_some() {
// floor to nearest 5% to reduce message spam
let new_progress = (downloaded as f64 / total_size.unwrap() as f64 * 20.0).floor() as i16 * 5;
if new_progress > last_progress {
last_progress = new_progress;
progress(last_progress);
}
}
}
Ok(())
}
pub fn download_url_as_string(url: &str) -> Result<String> {
let agent = get_download_agent()?;
let r = agent.get(url).call()?.into_string()?;
Ok(r)
}
fn get_download_agent() -> Result<ureq::Agent> {
#[allow(unused_mut)]
let mut tls_builder = native_tls::TlsConnector::builder();
#[cfg(target_os = "windows")]
if !crate::windows::is_windows_10_or_greater() {
warn!("DANGER: Discontinued OS version. TLS certificate verification will be disabled.");
warn!("DANGER: Discontinued OS version. TLS certificate verification will be disabled.");
warn!("DANGER: Discontinued OS version. TLS certificate verification will be disabled.");
tls_builder.danger_accept_invalid_certs(true);
tls_builder.danger_accept_invalid_hostnames(true);
}
let tls_connector = tls_builder.build()?;
Ok(ureq::AgentBuilder::new().tls_connector(tls_connector.into()).build())
}
#[test]
fn test_download_uses_tls_and_encoding_correctly() {
assert_eq!(
download_url_as_string("https://dotnetcli.blob.core.windows.net/dotnet/WindowsDesktop/5.0/latest.version").unwrap(),
"5.0.17"
);
}
#[test]
fn test_download_file_reports_progress() {
// https://www.ip-toolbox.com/speedtest-files/
let test_file = "https://proof.ovh.net/files/10Mb.dat";
let mut prog_count = 0;
let mut last_prog = 0;
download_url_to_file(test_file, "test_download_file_reports_progress.txt", |p| {
assert!(p >= last_prog);
prog_count += 1;
last_prog = p;
})
.unwrap();
assert!(prog_count >= 4);
assert!(prog_count <= 20);
assert_eq!(last_prog, 100);
let p = std::path::Path::new("test_download_file_reports_progress.txt");
let meta = p.metadata().unwrap();
let len = meta.len();
assert_eq!(len, 10 * 1024 * 1024);
std::fs::remove_file(p).unwrap();
}

View File

@@ -1,5 +1,5 @@
pub mod bundle; // pub mod bundle;
pub mod download; // pub mod download;
pub mod macho; pub mod macho;
pub mod runtime_arch; pub mod runtime_arch;

View File

@@ -13,7 +13,8 @@ use windows::Wdk::System::Threading::{NtQueryInformationProcess, ProcessBasicInf
use windows::Win32::System::Threading::{GetCurrentProcess, PROCESS_BASIC_INFORMATION}; use windows::Win32::System::Threading::{GetCurrentProcess, PROCESS_BASIC_INFORMATION};
use winsafe::{self as w, co, prelude::*}; use winsafe::{self as w, co, prelude::*};
use super::bundle::{self, EntryNameInfo, Manifest}; use velopack::bundle::{self, EntryNameInfo};
use velopack::locator::VelopackLocator;
pub fn wait_for_pid_to_exit(pid: u32, ms_to_wait: u32) -> Result<()> { pub fn wait_for_pid_to_exit(pid: u32, ms_to_wait: u32) -> Result<()> {
info!("Waiting {}ms for process ({}) to exit.", ms_to_wait, pid); info!("Waiting {}ms for process ({}) to exit.", ms_to_wait, pid);
@@ -169,12 +170,10 @@ fn _force_stop_package<P: AsRef<Path>>(root_dir: P) -> Result<()> {
Ok(()) Ok(())
} }
pub fn start_package<P: AsRef<Path>>(app: &Manifest, root_dir: P, exe_args: Option<Vec<&str>>, set_env: Option<&str>) -> Result<()> { pub fn start_package(locator: &VelopackLocator, exe_args: Option<Vec<&str>>, set_env: Option<&str>) -> Result<()> {
let root_dir = root_dir.as_ref().to_path_buf(); let current = locator.get_current_bin_dir();
let current = app.get_current_path(&root_dir); let exe_to_execute = locator.get_main_exe_path();
let exe = app.get_main_exe_path(&root_dir);
let exe_to_execute = std::path::Path::new(&exe);
if !exe_to_execute.exists() { if !exe_to_execute.exists() {
bail!("Unable to find executable to start: '{}'", exe_to_execute.to_string_lossy()); bail!("Unable to find executable to start: '{}'", exe_to_execute.to_string_lossy());
} }
@@ -189,7 +188,7 @@ pub fn start_package<P: AsRef<Path>>(app: &Manifest, root_dir: P, exe_args: Opti
psi.env(env, "true"); psi.env(env, "true");
} }
info!("About to launch: '{}' in dir '{}'", exe_to_execute.to_string_lossy(), current); info!("About to launch: '{:?}' in dir '{:?}'", exe_to_execute, current);
info!("Args: {:?}", psi.get_args()); info!("Args: {:?}", psi.get_args());
let child = psi.spawn().map_err(|z| anyhow!("Failed to start application ({}).", z))?; let child = psi.spawn().map_err(|z| anyhow!("Failed to start application ({}).", z))?;
let _ = unsafe { AllowSetForegroundWindow(child.id()) }; let _ = unsafe { AllowSetForegroundWindow(child.id()) };
@@ -197,20 +196,6 @@ pub fn start_package<P: AsRef<Path>>(app: &Manifest, root_dir: P, exe_args: Opti
Ok(()) Ok(())
} }
pub fn detect_manifest_from_update_path(update_exe: &PathBuf) -> Result<(PathBuf, Manifest)> {
let root_path = update_exe.parent().unwrap().to_path_buf();
let app = find_manifest_from_root_dir(&root_path)
.map_err(|m| anyhow!("Unable to read application manifest ({}). Is this a properly installed application?", m))?;
info!("Loaded manifest for application: {}", app.id);
info!("Root Directory: {}", root_path.to_string_lossy());
Ok((root_path, app))
}
pub fn detect_current_manifest() -> Result<(PathBuf, Manifest)> {
let me = std::env::current_exe()?;
detect_manifest_from_update_path(&me)
}
pub fn get_app_prefixed_folders<P: AsRef<Path>>(parent_path: P) -> Result<Vec<PathBuf>> { pub fn get_app_prefixed_folders<P: AsRef<Path>>(parent_path: P) -> Result<Vec<PathBuf>> {
let parent_path = parent_path.as_ref(); let parent_path = parent_path.as_ref();
let re = Regex::new(r"(?i)^app-")?; let re = Regex::new(r"(?i)^app-")?;
@@ -277,37 +262,37 @@ fn parse_version_from_folder_name(folder_name: &str) -> Option<Version> {
folder_name.strip_prefix("app-").and_then(|v| Version::parse(v).ok()) folder_name.strip_prefix("app-").and_then(|v| Version::parse(v).ok())
} }
fn find_manifest_from_root_dir(root_path: &PathBuf) -> Result<Manifest> { // fn find_manifest_from_root_dir(root_path: &PathBuf) -> Result<Manifest> {
// default to checking current/sq.version // // default to checking current/sq.version
let cm = find_current_manifest(root_path); // let cm = find_current_manifest(root_path);
if cm.is_ok() { // if cm.is_ok() {
return cm; // return cm;
} // }
//
// // if that fails, check for latest full package
// warn!("Unable to find current manifest, checking for latest full package. (LEGACY MODE)");
// let latest = find_latest_full_package(root_path);
// if let Some(latest) = latest {
// let mani = latest.load_manifest()?;
// return Ok(mani);
// }
//
// bail!("Unable to locate manifest or package.");
// }
//
// fn find_current_manifest(root_path: &PathBuf) -> Result<Manifest> {
// let m = Manifest::default();
// let nuspec_path = m.get_nuspec_path(root_path);
// if Path::new(&nuspec_path).exists() {
// if let Ok(nuspec) = super::retry_io(|| std::fs::read_to_string(&nuspec_path)) {
// return Ok(bundle::read_manifest_from_string(&nuspec)?);
// }
// }
// bail!("Unable to read nuspec file in current directory.")
// }
// if that fails, check for latest full package pub fn find_latest_full_package(locator: &VelopackLocator) -> Option<EntryNameInfo> {
warn!("Unable to find current manifest, checking for latest full package. (LEGACY MODE)"); let packages = get_all_packages(locator);
let latest = find_latest_full_package(root_path);
if let Some(latest) = latest {
let mani = latest.load_manifest()?;
return Ok(mani);
}
bail!("Unable to locate manifest or package.");
}
fn find_current_manifest(root_path: &PathBuf) -> Result<Manifest> {
let m = Manifest::default();
let nuspec_path = m.get_nuspec_path(root_path);
if Path::new(&nuspec_path).exists() {
if let Ok(nuspec) = super::retry_io(|| std::fs::read_to_string(&nuspec_path)) {
return Ok(bundle::read_manifest_from_string(&nuspec)?);
}
}
bail!("Unable to read nuspec file in current directory.")
}
pub fn find_latest_full_package(root_path: &PathBuf) -> Option<EntryNameInfo> {
let packages = get_all_packages(root_path);
let mut latest: Option<EntryNameInfo> = None; let mut latest: Option<EntryNameInfo> = None;
for pkg in packages { for pkg in packages {
if pkg.is_delta { if pkg.is_delta {
@@ -325,15 +310,14 @@ pub fn find_latest_full_package(root_path: &PathBuf) -> Option<EntryNameInfo> {
latest latest
} }
fn get_all_packages(root_path: &PathBuf) -> Vec<EntryNameInfo> { fn get_all_packages(locator: &VelopackLocator) -> Vec<EntryNameInfo> {
let m = Manifest::default(); let packages = locator.get_packages_dir();
let packages = m.get_packages_path(root_path);
let mut vec = Vec::new(); let mut vec = Vec::new();
debug!("Scanning for packages in {:?}", packages); debug!("Scanning for packages in {:?}", packages);
if let Ok(entries) = std::fs::read_dir(packages) { if let Ok(entries) = fs::read_dir(packages) {
for entry in entries { for entry in entries {
if let Ok(entry) = entry { if let Ok(entry) = entry {
if let Some(pkg) = super::bundle::parse_package_file_path(entry.path()) { if let Some(pkg) = bundle::parse_package_file_path(entry.path()) {
debug!("Found package: {}", entry.path().to_string_lossy()); debug!("Found package: {}", entry.path().to_string_lossy());
vec.push(pkg); vec.push(pkg);
} }

View File

@@ -7,6 +7,8 @@ extern crate log;
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use clap::{arg, value_parser, ArgMatches, Command}; use clap::{arg, value_parser, ArgMatches, Command};
use std::{env, path::PathBuf}; use std::{env, path::PathBuf};
use velopack::locator;
use velopack::locator::{auto_locate_app_manifest, LocationContext};
use velopack_bins::*; use velopack_bins::*;
#[rustfmt::skip] #[rustfmt::skip]
@@ -20,7 +22,7 @@ fn root_command() -> Command {
.arg(arg!(-w --wait "Wait for the parent process to terminate before applying the update").hide(true)) .arg(arg!(-w --wait "Wait for the parent process to terminate before applying the update").hide(true))
.arg(arg!(--waitPid <PID> "Wait for the specified process to terminate before applying the update").value_parser(value_parser!(u32))) .arg(arg!(--waitPid <PID> "Wait for the specified process to terminate before applying the update").value_parser(value_parser!(u32)))
.arg(arg!(-p --package <FILE> "Update package to apply").value_parser(value_parser!(PathBuf))) .arg(arg!(-p --package <FILE> "Update package to apply").value_parser(value_parser!(PathBuf)))
.arg(arg!([EXE_ARGS] "Arguments to pass to the started executable. Must be preceeded by '--'.").required(false).last(true).num_args(0..)) .arg(arg!([EXE_ARGS] "Arguments to pass to the started executable. Must be preceded by '--'.").required(false).last(true).num_args(0..))
) )
.subcommand(Command::new("start") .subcommand(Command::new("start")
.about("Starts the currently installed version of the application") .about("Starts the currently installed version of the application")
@@ -28,7 +30,7 @@ fn root_command() -> Command {
.arg(arg!(-w --wait "Wait for the parent process to terminate before starting the application").hide(true)) .arg(arg!(-w --wait "Wait for the parent process to terminate before starting the application").hide(true))
.arg(arg!(--waitPid <PID> "Wait for the specified process to terminate before applying the update").value_parser(value_parser!(u32))) .arg(arg!(--waitPid <PID> "Wait for the specified process to terminate before applying the update").value_parser(value_parser!(u32)))
.arg(arg!([EXE_NAME] "The optional name of the binary to execute")) .arg(arg!([EXE_NAME] "The optional name of the binary to execute"))
.arg(arg!([EXE_ARGS] "Arguments to pass to the started executable. Must be preceeded by '--'.").required(false).last(true).num_args(0..)) .arg(arg!([EXE_ARGS] "Arguments to pass to the started executable. Must be preceded by '--'.").required(false).last(true).num_args(0..))
.long_flag_aliases(vec!["processStart", "processStartAndWait"]) .long_flag_aliases(vec!["processStart", "processStartAndWait"])
) )
.subcommand(Command::new("patch") .subcommand(Command::new("patch")
@@ -41,11 +43,13 @@ fn root_command() -> Command {
.about("Prints the current version of the application") .about("Prints the current version of the application")
) )
.arg(arg!(--verbose "Print debug messages to console / log").global(true)) .arg(arg!(--verbose "Print debug messages to console / log").global(true))
.arg(arg!(--nocolor "Disable colored output").hide(true).global(true))
.arg(arg!(-s --silent "Don't show any prompts / dialogs").global(true)) .arg(arg!(-s --silent "Don't show any prompts / dialogs").global(true))
.arg(arg!(-l --log <PATH> "Override the default log file location").global(true).value_parser(value_parser!(PathBuf))) .arg(arg!(-l --log <PATH> "Override the default log file location").global(true).value_parser(value_parser!(PathBuf)))
// Legacy arguments should not be fully removed if it's possible to keep them
// Reason being is clap.ignore_errors(true) is not 100%, and sometimes old args can trip things up.
.arg(arg!(--forceLatest "Legacy argument").hide(true).global(true)) .arg(arg!(--forceLatest "Legacy argument").hide(true).global(true))
.arg(arg!(-r --restart "Legacy argument").hide(true).global(true)) .arg(arg!(-r --restart "Legacy argument").hide(true).global(true))
.arg(arg!(--nocolor "Legacy argument").hide(true).global(true))
.ignore_errors(true) .ignore_errors(true)
.disable_help_subcommand(true) .disable_help_subcommand(true)
.flatten_help(true); .flatten_help(true);
@@ -122,17 +126,12 @@ fn main() -> Result<()> {
let verbose = get_flag_or_false(&matches, "verbose"); let verbose = get_flag_or_false(&matches, "verbose");
let silent = get_flag_or_false(&matches, "silent"); let silent = get_flag_or_false(&matches, "silent");
let nocolor = get_flag_or_false(&matches, "nocolor");
let log_file = matches.get_one("log"); let log_file = matches.get_one("log");
dialogs::set_silent(silent); dialogs::set_silent(silent);
if let Some(log_file) = log_file { let desired_log_file = log_file.cloned().unwrap_or(locator::default_log_location(LocationContext::IAmUpdateExe));
logging::setup_logging("update", Some(&log_file), true, verbose, nocolor)?; logging::setup_logging("update", Some(&desired_log_file), true, verbose)?;
} else {
let default_log_file = logging::default_log_location();
logging::setup_logging("update", Some(&default_log_file), true, verbose, nocolor)?;
}
// change working directory to the parent directory of the exe // change working directory to the parent directory of the exe
let mut containing_dir = env::current_exe()?; let mut containing_dir = env::current_exe()?;
containing_dir.pop(); containing_dir.pop();
@@ -173,7 +172,8 @@ fn patch(matches: &ArgMatches) -> Result<()> {
info!(" Patch File: {:?}", patch_file); info!(" Patch File: {:?}", patch_file);
info!(" Output File: {:?}", output_file); info!(" Output File: {:?}", output_file);
commands::patch(old_file, patch_file, output_file) velopack::delta::zstd_patch_single(old_file, patch_file, output_file)?;
Ok(())
} }
fn apply(matches: &ArgMatches) -> Result<()> { fn apply(matches: &ArgMatches) -> Result<()> {
@@ -188,10 +188,10 @@ fn apply(matches: &ArgMatches) -> Result<()> {
info!(" Package: {:?}", package); info!(" Package: {:?}", package);
info!(" Exe Args: {:?}", exe_args); info!(" Exe Args: {:?}", exe_args);
let (root_path, app) = shared::detect_current_manifest()?; let locator = auto_locate_app_manifest(LocationContext::IAmUpdateExe)?;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let _mutex = shared::retry_io(|| windows::create_global_mutex(&app))?; let _mutex = shared::retry_io(|| windows::create_global_mutex(&locator.get_manifest_id()))?;
commands::apply(&root_path, &app, restart, wait, package, exe_args, true) commands::apply(&locator, restart, wait, package, exe_args, true)
} }
fn start(matches: &ArgMatches) -> Result<()> { fn start(matches: &ArgMatches) -> Result<()> {
@@ -209,17 +209,17 @@ fn start(matches: &ArgMatches) -> Result<()> {
warn!("Legacy args format is deprecated and will be removed in a future release. Please update your application to use the new format."); warn!("Legacy args format is deprecated and will be removed in a future release. Please update your application to use the new format.");
} }
let (root_path, app) = shared::detect_current_manifest()?; let locator = auto_locate_app_manifest(LocationContext::IAmUpdateExe)?;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let _mutex = shared::retry_io(|| windows::create_global_mutex(&app))?; let _mutex = shared::retry_io(|| windows::create_global_mutex(&locator.get_manifest_id()))?;
commands::start(&root_path, &app, wait, exe_name, exe_args, legacy_args) commands::start(&locator, wait, exe_name, exe_args, legacy_args)
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn uninstall(_matches: &ArgMatches) -> Result<()> { fn uninstall(_matches: &ArgMatches) -> Result<()> {
info!("Command: Uninstall"); info!("Command: Uninstall");
let (root_path, app) = shared::detect_current_manifest()?; let locator = auto_locate_app_manifest(LocationContext::IAmUpdateExe)?;
commands::uninstall(&root_path, &app, true) commands::uninstall(&locator, true)
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]

View File

@@ -1,10 +1,13 @@
use crate::dialogs::{self, DialogResult};
use std::{ffi::OsStr, path::Path}; use std::{ffi::OsStr, path::Path};
use velopack::locator::VelopackLocator;
use crate::{bundle::Manifest, dialogs, dialogs::DialogResult}; pub fn close_processes_locking_dir(locator: &VelopackLocator) -> bool {
let app_title = locator.get_manifest_title();
pub fn close_processes_locking_dir(manifest: &Manifest, path: &str) -> bool { let app_version = locator.get_manifest_version_full_string();
loop { loop {
let pids = filelocksmith::find_processes_locking_path(path); let bin_dir = locator.get_current_bin_dir();
let pids = filelocksmith::find_processes_locking_path(&bin_dir);
if pids.is_empty() { if pids.is_empty() {
return true; return true;
} }
@@ -24,7 +27,7 @@ pub fn close_processes_locking_dir(manifest: &Manifest, path: &str) -> bool {
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(", "); .join(", ");
let result = dialogs::show_processes_locking_folder_dialog(manifest, &pids_str); let result = dialogs::show_processes_locking_folder_dialog(&app_title, &app_version, &pids_str);
match result { match result {
DialogResult::Retry => continue, DialogResult::Retry => continue,
@@ -41,8 +44,13 @@ pub fn close_processes_locking_dir(manifest: &Manifest, path: &str) -> bool {
#[test] #[test]
#[ignore] #[ignore]
fn test_close_processes_locking_dir() { fn test_close_processes_locking_dir() {
let mut mani = Manifest::default(); let mut paths = velopack::locator::VelopackLocatorConfig::default();
paths.CurrentBinaryDir = std::path::PathBuf::from(r"C:\Users\Caelan\AppData\Local\Clowd");
let mut mani = velopack::bundle::Manifest::default();
mani.title = "Test".to_owned(); mani.title = "Test".to_owned();
mani.version = semver::Version::parse("1.0.0").unwrap(); mani.version = semver::Version::parse("1.0.0").unwrap();
close_processes_locking_dir(&mani, r"C:\Users\Caelan\AppData\Local\Clowd");
let locator = VelopackLocator::new(paths.clone(), mani.clone());
close_processes_locking_dir(&locator);
} }

View File

@@ -5,6 +5,7 @@ pub mod runtimes;
pub mod splash; pub mod splash;
pub mod known_path; pub mod known_path;
pub mod strings; pub mod strings;
pub mod registry;
mod self_delete; mod self_delete;
mod shortcuts; mod shortcuts;

View File

@@ -1,5 +1,6 @@
use super::{runtimes, splash}; use super::{runtimes, splash};
use crate::shared::{bundle, dialogs, download}; use crate::shared::dialogs;
use velopack::{bundle, download};
use anyhow::Result; use anyhow::Result;
use std::path::Path; use std::path::Path;

View File

@@ -0,0 +1,53 @@
use anyhow::Result;
use chrono::{Datelike, Local as DateTime};
use velopack::locator::VelopackLocator;
use winsafe::{self as w, co, prelude::*};
const UNINSTALL_REGISTRY_KEY: &'static str = "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall";
pub fn write_uninstall_entry(locator: &VelopackLocator) -> Result<()> {
info!("Writing uninstall registry key...");
let app_id = locator.get_manifest_id();
let app_title = locator.get_manifest_title();
let app_authors = locator.get_manifest_authors();
let root_path_str = locator.get_root_dir_as_string();
let main_exe_path = locator.get_main_exe_path_as_string();
let updater_path = locator.get_update_path_as_string();
let folder_size = fs_extra::dir::get_size(locator.get_root_dir()).unwrap_or(0);
let short_version = locator.get_manifest_version_short_string();
let now = DateTime::now();
let formatted_date = format!("{}{:02}{:02}", now.year(), now.month(), now.day());
let uninstall_cmd = format!("\"{}\" --uninstall", updater_path);
let uninstall_quiet = format!("\"{}\" --uninstall --silent", updater_path);
let reg_uninstall =
w::HKEY::CURRENT_USER.RegCreateKeyEx(UNINSTALL_REGISTRY_KEY, None, co::REG_OPTION::NoValue, co::KEY::CREATE_SUB_KEY, None)?.0;
let reg_app = reg_uninstall.RegCreateKeyEx(&app_id, None, co::REG_OPTION::NoValue, co::KEY::ALL_ACCESS, None)?.0;
reg_app.RegSetKeyValue(None, Some("DisplayIcon"), w::RegistryValue::Sz(main_exe_path))?;
reg_app.RegSetKeyValue(None, Some("DisplayName"), w::RegistryValue::Sz(app_title))?;
reg_app.RegSetKeyValue(None, Some("DisplayVersion"), w::RegistryValue::Sz(short_version))?;
reg_app.RegSetKeyValue(None, Some("InstallDate"), w::RegistryValue::Sz(formatted_date))?;
reg_app.RegSetKeyValue(None, Some("InstallLocation"), w::RegistryValue::Sz(root_path_str))?;
reg_app.RegSetKeyValue(None, Some("Publisher"), w::RegistryValue::Sz(app_authors))?;
reg_app.RegSetKeyValue(None, Some("QuietUninstallString"), w::RegistryValue::Sz(uninstall_quiet))?;
reg_app.RegSetKeyValue(None, Some("UninstallString"), w::RegistryValue::Sz(uninstall_cmd))?;
reg_app.RegSetKeyValue(None, Some("EstimatedSize"), w::RegistryValue::Dword((folder_size / 1024).try_into()?))?;
reg_app.RegSetKeyValue(None, Some("NoModify"), w::RegistryValue::Dword(1))?;
reg_app.RegSetKeyValue(None, Some("NoRepair"), w::RegistryValue::Dword(1))?;
reg_app.RegSetKeyValue(None, Some("Language"), w::RegistryValue::Dword(0x0409))?;
Ok(())
}
pub fn remove_uninstall_entry(locator: &VelopackLocator) -> Result<()> {
info!("Removing uninstall registry keys...");
let app_id = locator.get_manifest_id();
let reg_uninstall =
w::HKEY::CURRENT_USER.RegCreateKeyEx(UNINSTALL_REGISTRY_KEY, None, co::REG_OPTION::NoValue, co::KEY::CREATE_SUB_KEY, None)?.0;
reg_uninstall.RegDeleteKey(&app_id)?;
Ok(())
}

View File

@@ -1,12 +1,14 @@
use crate::shared as util; use crate::shared as util;
use crate::shared::download;
use crate::shared::runtime_arch::RuntimeArch; use crate::shared::runtime_arch::RuntimeArch;
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use regex::Regex; use regex::Regex;
use std::process::Command as Process; use std::process::Command as Process;
use std::{collections::HashMap, fs, path::Path}; use std::{collections::HashMap, fs, path::Path};
use velopack::download;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use winsafe::{self as w, co, prelude::*}; use winsafe::{self as w, co, prelude::*};
const REDIST_2015_2022_X86: &str = "https://aka.ms/vs/17/release/vc_redist.x86.exe"; const REDIST_2015_2022_X86: &str = "https://aka.ms/vs/17/release/vc_redist.x86.exe";
const REDIST_2015_2022_X64: &str = "https://aka.ms/vs/17/release/vc_redist.x64.exe"; const REDIST_2015_2022_X64: &str = "https://aka.ms/vs/17/release/vc_redist.x64.exe";
const REDIST_2015_2022_ARM64: &str = "https://aka.ms/vs/17/release/vc_redist.arm64.exe"; const REDIST_2015_2022_ARM64: &str = "https://aka.ms/vs/17/release/vc_redist.arm64.exe";

View File

@@ -3,63 +3,30 @@ use std::path::{Path, PathBuf};
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, bail, Result}; use anyhow::{anyhow, bail, Result};
use bitflags::bitflags;
use glob::glob; use glob::glob;
use same_file::is_same_file; use same_file::is_same_file;
use windows::core::{Interface, GUID, PCWSTR}; use velopack::{locator::ShortcutLocationFlags, locator::VelopackLocator};
use windows::core::{Interface, GUID, PCWSTR, PROPVARIANT};
use windows::Win32::Storage::EnhancedStorage::PKEY_AppUserModel_ID; use windows::Win32::Storage::EnhancedStorage::PKEY_AppUserModel_ID;
use windows::Win32::System::Com::{ use windows::Win32::System::Com::{
CoCreateInstance, CoInitializeEx, CoUninitialize, IPersistFile, StructuredStorage::InitPropVariantFromStringVector, CLSCTX_ALL, CoCreateInstance, CoInitializeEx, CoUninitialize, IPersistFile, StructuredStorage::InitPropVariantFromStringVector,
COINIT_APARTMENTTHREADED, COINIT_DISABLE_OLE1DDE, STGM_READWRITE, CLSCTX_ALL, COINIT_APARTMENTTHREADED, COINIT_DISABLE_OLE1DDE, STGM_READWRITE,
}; };
use windows::Win32::UI::Shell::{ use windows::Win32::UI::Shell::{
IShellItem, IShellLinkW, IStartMenuPinnedList, PropertiesSystem::IPropertyStore, SHCreateItemFromParsingName, ShellLink, StartMenuPin, IShellItem, IShellLinkW, IStartMenuPinnedList, PropertiesSystem::IPropertyStore, SHCreateItemFromParsingName, ShellLink, StartMenuPin,
}; };
use crate::bundle::Manifest;
use crate::shared as util; use crate::shared as util;
use crate::windows::{known_path as known, strings::*}; use crate::windows::{known_path as known, strings::*};
// https://github.com/vaginessa/PWAsForFirefox/blob/fba68dbcc7ca27b970dc5a278ebdad32e0ab3c83/native/src/integrations/implementation/windows.rs#L28 // https://github.com/vaginessa/PWAsForFirefox/blob/fba68dbcc7ca27b970dc5a278ebdad32e0ab3c83/native/src/integrations/implementation/windows.rs#L28
bitflags! { pub fn create_or_update_manifest_lnks(next_app: &VelopackLocator, previous_app: Option<&VelopackLocator>) {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct ShortcutLocationFlags: u32 {
const NONE = 0;
const START_MENU = 1 << 0;
const DESKTOP = 1 << 1;
const STARTUP = 1 << 2;
//const APP_ROOT = 1 << 3,
const START_MENU_ROOT = 1 << 4;
const USER_PINNED = 1 << 5;
}
}
impl ShortcutLocationFlags {
fn from_string(input: &str) -> ShortcutLocationFlags {
let mut flags = ShortcutLocationFlags::NONE;
for part in input.split(|c| c == ',' || c == ';') {
match part.trim().to_lowercase().as_str() {
"none" => flags |= ShortcutLocationFlags::NONE,
"startmenu" => flags |= ShortcutLocationFlags::START_MENU,
"desktop" => flags |= ShortcutLocationFlags::DESKTOP,
"startup" => flags |= ShortcutLocationFlags::STARTUP,
"startmenuroot" => flags |= ShortcutLocationFlags::START_MENU_ROOT,
_ => warn!("Warning: Unrecognized shortcut flag `{}`", part.trim()),
}
}
flags
}
}
pub fn create_or_update_manifest_lnks<P: AsRef<Path>>(root_path: P, next_app: &Manifest, previous_app: Option<&Manifest>) {
let root_path = root_path.as_ref().to_owned().to_path_buf();
let next_app = next_app.clone(); let next_app = next_app.clone();
let previous_app = previous_app.cloned(); let previous_app = previous_app.cloned();
unsafe { unsafe {
if let Err(e) = unsafe_run_delegate_in_com_context(move || { if let Err(e) = unsafe_run_delegate_in_com_context(move || {
unsafe_update_app_manifest_lnks(&root_path, &next_app, previous_app.as_ref())?; unsafe_update_app_manifest_lnks(&next_app, previous_app.as_ref())?;
Ok(()) Ok(())
}) { }) {
warn!("Failed to update shortcuts: {}", e); warn!("Failed to update shortcuts: {}", e);
@@ -84,59 +51,53 @@ unsafe fn create_instance<T: Interface>(clsid: &GUID) -> Result<T> {
Ok(CoCreateInstance(clsid, None, CLSCTX_ALL)?) Ok(CoCreateInstance(clsid, None, CLSCTX_ALL)?)
} }
fn get_shortcut_filename(app: &Manifest) -> String { fn get_shortcut_filename(app_id: &str, app_title: &str) -> String {
let name = if app.title.is_empty() { app.id.clone() } else { app.title.clone() }; let name = if app_title.is_empty() { app_id.to_owned() } else { app_title.to_owned() };
let shortcut_file_name = name + ".lnk"; let shortcut_file_name = name + ".lnk";
shortcut_file_name shortcut_file_name
} }
fn get_path_for_shortcut_location(app: &Manifest, flag: ShortcutLocationFlags) -> Result<PathBuf> { fn get_path_for_shortcut_location(app_id: &str, app_title: &str, app_author: &str, flag: ShortcutLocationFlags) -> Result<PathBuf> {
let shortcut_file_name = get_shortcut_filename(app); let shortcut_file_name = get_shortcut_filename(app_id, app_title);
match flag { match flag {
ShortcutLocationFlags::DESKTOP => Ok(Path::new(&known::get_user_desktop()?).join(shortcut_file_name)), ShortcutLocationFlags::DESKTOP => Ok(Path::new(&known::get_user_desktop()?).join(shortcut_file_name)),
ShortcutLocationFlags::STARTUP => Ok(Path::new(&known::get_startup()?).join(shortcut_file_name)), ShortcutLocationFlags::STARTUP => Ok(Path::new(&known::get_startup()?).join(shortcut_file_name)),
ShortcutLocationFlags::START_MENU_ROOT => Ok(Path::new(&known::get_start_menu()?).join(shortcut_file_name)), ShortcutLocationFlags::START_MENU_ROOT => Ok(Path::new(&known::get_start_menu()?).join(shortcut_file_name)),
ShortcutLocationFlags::START_MENU => { ShortcutLocationFlags::START_MENU => {
if app.authors.is_empty() { if app_author.is_empty() {
warn!("No authors specified and START_MENU shortcut specified. Using START_MENU_ROOT instead."); warn!("No authors specified and START_MENU shortcut specified. Using START_MENU_ROOT instead.");
Ok(Path::new(&known::get_start_menu()?).join(shortcut_file_name)) Ok(Path::new(&known::get_start_menu()?).join(shortcut_file_name))
} else { } else {
Ok(Path::new(&known::get_start_menu()?).join(&app.authors).join(shortcut_file_name)) Ok(Path::new(&known::get_start_menu()?).join(app_author).join(shortcut_file_name))
} }
} }
_ => bail!("Invalid shortcut location flag: {:?}", flag), _ => bail!("Invalid shortcut location flag: {:?}", flag),
} }
} }
unsafe fn unsafe_update_app_manifest_lnks(root_path: &PathBuf, next_app: &Manifest, previous_app: Option<&Manifest>) -> Result<()> { unsafe fn unsafe_update_app_manifest_lnks(next_app: &VelopackLocator, previous_app: Option<&VelopackLocator>) -> Result<()> {
let next_locations = ShortcutLocationFlags::from_string(&next_app.shortcut_locations); let next_locations = next_app.get_manifest_shortcut_locations();
let prev_locations = if let Some(prev) = previous_app { let prev_locations = previous_app.map(|a| a.get_manifest_shortcut_locations()).unwrap_or(ShortcutLocationFlags::NONE);
ShortcutLocationFlags::from_string(&prev.shortcut_locations)
} else { info!("Shortcut Previous Locations: {:?} ({:?})", prev_locations, previous_app.map(|a| a.get_manifest_version_full_string()));
ShortcutLocationFlags::NONE info!("Shortcut Next Locations: {:?} ({:?})", next_locations, next_app.get_manifest_version_full_string());
};
info!("Shortcut Previous Locations: {:?} ({:?})", prev_locations, previous_app.map(|a| a.version.clone()));
info!("Shortcut Next Locations: {:?} ({:?})", next_locations, next_app.version);
// we must end with shortcuts which exist in the next app but not the previous app. // we must end with shortcuts which exist in the next app but not the previous app.
// any shortcuts which exist in both are optional - they could have been deleted by the user, // any shortcuts which exist in both are optional - they could have been deleted by the user,
// and we do not want to re-create them. // and we do not want to re-create them.
let mut new_locations = next_locations - prev_locations - ShortcutLocationFlags::NONE; let mut new_locations = next_locations - prev_locations;
if new_locations.contains(ShortcutLocationFlags::START_MENU_ROOT) && new_locations.contains(ShortcutLocationFlags::START_MENU) { if new_locations.contains(ShortcutLocationFlags::START_MENU_ROOT) && new_locations.contains(ShortcutLocationFlags::START_MENU) {
// if both start menu locations are specified, we prefer ROOT. // if both start menu locations are specified, we prefer ROOT.
new_locations.remove(ShortcutLocationFlags::START_MENU); new_locations.remove(ShortcutLocationFlags::START_MENU);
} }
let root_path = next_app.get_root_dir();
let mut app_model_id: Option<String> = None; let app_id = next_app.get_manifest_id();
if !next_app.shortcut_amuid.is_empty() { let app_title = next_app.get_manifest_title();
app_model_id = Some(next_app.shortcut_amuid.clone()); let app_authors = next_app.get_manifest_authors();
} let app_model_id: Option<String> = next_app.get_manifest_shortcut_amuid();
let app_main_exe = next_app.get_main_exe_path_as_string();
let app_main_exe = next_app.get_main_exe_path(root_path); let app_work_dir = next_app.get_current_bin_dir_as_string();
let app_work_dir = next_app.get_current_path(root_path);
info!("App Model ID: {:?}", app_model_id); info!("App Model ID: {:?}", app_model_id);
let mut current_shortcuts = unsafe_get_shortcuts_for_root_dir(root_path)?; let mut current_shortcuts = unsafe_get_shortcuts_for_root_dir(root_path)?;
@@ -184,15 +145,15 @@ unsafe fn unsafe_update_app_manifest_lnks(root_path: &PathBuf, next_app: &Manife
} }
// rename existing shortcuts if packTitle has changed // rename existing shortcuts if packTitle has changed
let last_app_name = previous_app.map(|a| a.title.clone()).unwrap_or(next_app.title.clone()); let last_app_name = previous_app.map(|a| a.get_manifest_title()).unwrap_or(app_title.clone());
let shortcuts_to_rename = unsafe_find_best_rename_candidates(&last_app_name, &app_main_exe, current_shortcuts); let shortcuts_to_rename = unsafe_find_best_rename_candidates(&last_app_name, &app_main_exe, current_shortcuts);
for (flag, path) in shortcuts_to_rename { for (flag, path) in shortcuts_to_rename {
let shortcut_file_name = get_shortcut_filename(next_app); let shortcut_file_name = get_shortcut_filename(&app_id, &app_title);
let target_path = if let Some(parent) = path.parent() { let target_path = if let Some(parent) = path.parent() {
parent.join(shortcut_file_name) parent.join(shortcut_file_name)
} else { } else {
get_path_for_shortcut_location(next_app, flag)? get_path_for_shortcut_location(&app_id, &app_title, &app_authors, flag)?
}; };
if path != target_path { if path != target_path {
@@ -205,19 +166,17 @@ unsafe fn unsafe_update_app_manifest_lnks(root_path: &PathBuf, next_app: &Manife
// add new (missing) shortcut locations // add new (missing) shortcut locations
for flag in new_locations.iter() { for flag in new_locations.iter() {
let path = get_path_for_shortcut_location(next_app, flag)?; let path = get_path_for_shortcut_location(&app_id, &app_title, &app_authors, flag)?;
let target = next_app.get_main_exe_path(root_path);
let work_dir = next_app.get_current_path(root_path);
info!("Creating new shortcut for flag '{:?}' ({:?}).", path, flag); info!("Creating new shortcut for flag '{:?}' ({:?}).", path, flag);
match Lnk::create_new() { match Lnk::create_new() {
Ok(mut lnk) => { Ok(mut lnk) => {
if let Err(e) = lnk.set_target_path(&target) { if let Err(e) = lnk.set_target_path(&app_main_exe) {
warn!("Failed to set target path: {}", e); warn!("Failed to set target path: {}", e);
break; break;
} }
if let Err(e) = lnk.set_working_directory(&work_dir) { if let Err(e) = lnk.set_working_directory(&app_work_dir) {
warn!("Failed to set working directory: {}", e); warn!("Failed to set working directory: {}", e);
break; break;
} }
@@ -227,7 +186,7 @@ unsafe fn unsafe_update_app_manifest_lnks(root_path: &PathBuf, next_app: &Manife
break; break;
} }
if let Err(e) = lnk.set_icon_location(&target, 0) { if let Err(e) = lnk.set_icon_location(&app_main_exe, 0) {
warn!("Failed to set icon location: {}", e); warn!("Failed to set icon location: {}", e);
break; break;
} }
@@ -514,16 +473,11 @@ impl Lnk {
let id = PCWSTR(id.as_ptr()); let id = PCWSTR(id.as_ptr());
let variant = InitPropVariantFromStringVector(Some(&[id]))?; let variant = InitPropVariantFromStringVector(Some(&[id]))?;
store.SetValue(&PKEY_AppUserModel_ID, &variant)?; store.SetValue(&PKEY_AppUserModel_ID, &variant)?;
store.Commit()?; } else {
let prop_variant = PROPVARIANT::default(); // defaults to VT_EMPTY
store.SetValue(&PKEY_AppUserModel_ID, &prop_variant)?;
} }
// TODO: add clear/remove branch store.Commit()?;
// else {
// let mut varient = PROPVARIANT::default();
// VariantToPropVariant(VT_EMPTY, &mut varient);
// store.SetValue(&PKEY_AppUserModel_ID, VT_EMPTY)?;
// VT_EMPTY
// initpropvariant
// }
Ok(()) Ok(())
} }
@@ -538,9 +492,6 @@ impl Lnk {
Ok(self.pf.Save(output, true)?) Ok(self.pf.Save(output, true)?)
} }
// const SLR_NO_UI: u32 = 1;
// const SLR_ANY_MATCH: u32 = 2;
// const TIMEOUT_1MS: u32 = 1 << 16;
pub unsafe fn open_write<P: AsRef<Path>>(link_path: P) -> Result<Lnk> { pub unsafe fn open_write<P: AsRef<Path>>(link_path: P) -> Result<Lnk> {
let link_path = link_path.as_ref().to_string_lossy().to_string(); let link_path = link_path.as_ref().to_string_lossy().to_string();
@@ -555,11 +506,16 @@ impl Lnk {
// we don't really want to "resolve" the shortcut in the middle of an update operation // we don't really want to "resolve" the shortcut in the middle of an update operation
// this can cause Windows to move the target path of a shortcut to one of our temp dirs etc // this can cause Windows to move the target path of a shortcut to one of our temp dirs etc
// const SLR_NO_UI: u32 = 1;
// const SLR_ANY_MATCH: u32 = 2;
// const TIMEOUT_1MS: u32 = 1 << 16;
// let flags = Lnk::SLR_NO_UI | Lnk::SLR_ANY_MATCH | Lnk::TIMEOUT_1MS; // let flags = Lnk::SLR_NO_UI | Lnk::SLR_ANY_MATCH | Lnk::TIMEOUT_1MS;
// if let Err(e) = link.Resolve(HWND(0), flags) { // if let Err(e) = link.Resolve(HWND(0), flags) {
// // this happens if the target path is missing and the link is broken // // this happens if the target path is missing and the link is broken
// warn!("Failed to resolve link {} ({:?})", link_path, e); // warn!("Failed to resolve link {} ({:?})", link_path, e);
// } // }
Ok(Lnk { me: link, pf: persist, my_path: link_path }) Ok(Lnk { me: link, pf: persist, my_path: link_path })
} }
@@ -576,27 +532,6 @@ impl std::fmt::Debug for Lnk {
} }
} }
// #[test]
// fn test_shortcut_matching() {
// let shortcuts = vec![
// (ShortcutLocationFlags::DESKTOP, PathBuf::from("C:\\Users\\Caelan\\Desktop\\Discor.lnk")),
// (ShortcutLocationFlags::DESKTOP, PathBuf::from("C:\\Users\\Caelan\\Desktop\\Discord Hello.lnk")),
// (ShortcutLocationFlags::DESKTOP, PathBuf::from("C:\\Users\\Caelan\\Desktop\\Discord.lnk")),
// (ShortcutLocationFlags::DESKTOP, PathBuf::from("C:\\Users\\Caelan\\Desktop\\Hello Hello.lnk")),
// (ShortcutLocationFlags::DESKTOP, PathBuf::from("C:\\Users\\Caelan\\Desktop\\DiscordDa.lnk")),
// (ShortcutLocationFlags::START_MENU_ROOT, PathBuf::from("C:\\Users\\Caelan\\Desktop\\asdasdasd3Discord2947230947kjsl.lnk")),
// (ShortcutLocationFlags::START_MENU_ROOT, PathBuf::from("C:\\Users\\Caelan\\Desktop\\a2947230947kjsl.lnk")),
// ];
//
// let matches = unsafe_find_best_shortcut_matches("Discord", shortcuts);
// assert_eq!(matches.len(), 2);
// assert_eq!(matches.get(&ShortcutLocationFlags::DESKTOP).unwrap(), &PathBuf::from("C:\\Users\\Caelan\\Desktop\\Discord.lnk"));
// assert_eq!(
// matches.get(&ShortcutLocationFlags::START_MENU_ROOT).unwrap(),
// &PathBuf::from("C:\\Users\\Caelan\\Desktop\\asdasdasd3Discord2947230947kjsl.lnk")
// );
// }
#[test] #[test]
#[ignore] #[ignore]
fn test_unpin_shortcut() { fn test_unpin_shortcut() {
@@ -606,7 +541,7 @@ fn test_unpin_shortcut() {
unsafe_unpin_lnk_from_start(path)?; unsafe_unpin_lnk_from_start(path)?;
Ok(()) Ok(())
}) })
.unwrap(); .unwrap();
} }
} }
#[test] #[test]
@@ -628,7 +563,7 @@ fn test_shortcut_intense_intermittent() {
l.save_as(&p1).unwrap(); l.save_as(&p1).unwrap();
Ok(()) Ok(())
}) })
.unwrap(); .unwrap();
} }
assert!(lnk_path.exists()); assert!(lnk_path.exists());
util::retry_io(|| std::fs::remove_file(&lnk_path)).unwrap(); util::retry_io(|| std::fs::remove_file(&lnk_path)).unwrap();
@@ -663,7 +598,7 @@ fn test_can_resolve_existing_shortcut() {
assert_eq!(target, "C:\\Users\\Caelan\\AppData\\Local\\Discord\\Update.exe"); assert_eq!(target, "C:\\Users\\Caelan\\AppData\\Local\\Discord\\Update.exe");
Ok(()) Ok(())
}) })
.unwrap(); .unwrap();
} }
} }
@@ -739,6 +674,6 @@ fn test_shortcut_full_integration() {
assert!(!start_menu_subfolder.exists()); assert!(!start_menu_subfolder.exists());
Ok(()) Ok(())
}) })
.unwrap(); .unwrap();
} }
} }

View File

@@ -5,6 +5,8 @@ use std::{
time::Duration, time::Duration,
}; };
use velopack::locator::VelopackLocator;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use normpath::PathExt; use normpath::PathExt;
use wait_timeout::ChildExt; use wait_timeout::ChildExt;
@@ -20,11 +22,12 @@ use windows::Win32::{
use crate::shared::{self, runtime_arch::RuntimeArch}; use crate::shared::{self, runtime_arch::RuntimeArch};
use crate::windows::strings::{string_to_u16, u16_to_string}; use crate::windows::strings::{string_to_u16, u16_to_string};
pub fn run_hook(app: &shared::bundle::Manifest, root_path: &PathBuf, hook_name: &str, timeout_secs: u64) -> bool { pub fn run_hook(locator: &VelopackLocator, hook_name: &str, timeout_secs: u64) -> bool {
let sw = simple_stopwatch::Stopwatch::start_new(); let sw = simple_stopwatch::Stopwatch::start_new();
let current_path = app.get_current_path(&root_path); let root_dir = locator.get_root_dir();
let main_exe_path = app.get_main_exe_path(&root_path); let current_path = locator.get_current_bin_dir();
let ver_string = app.version.to_string(); let main_exe_path = locator.get_main_exe_path();
let ver_string = locator.get_manifest_version_full_string();
let args = vec![hook_name, &ver_string]; let args = vec![hook_name, &ver_string];
let mut success = false; let mut success = false;
@@ -59,7 +62,7 @@ pub fn run_hook(app: &shared::bundle::Manifest, root_path: &PathBuf, hook_name:
} }
// in case the hook left running processes // in case the hook left running processes
let _ = shared::force_stop_package(&root_path); let _ = shared::force_stop_package(&root_dir);
success success
} }
@@ -75,8 +78,8 @@ impl Drop for MutexDropGuard {
} }
} }
pub fn create_global_mutex(app: &shared::bundle::Manifest) -> Result<MutexDropGuard> { pub fn create_global_mutex(app_id: &str) -> Result<MutexDropGuard> {
let mutex_name = format!("velopack-{}", &app.id); let mutex_name = format!("velopack-{}", app_id);
info!("Attempting to open global system mutex: '{}'", &mutex_name); info!("Attempting to open global system mutex: '{}'", &mutex_name);
let encodedu16 = super::strings::string_to_u16(mutex_name); let encodedu16 = super::strings::string_to_u16(mutex_name);
let encoded = PCWSTR(encodedu16.as_ptr()); let encoded = PCWSTR(encodedu16.as_ptr());

View File

@@ -8,6 +8,8 @@ use velopack_bins::*;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use winsafe::{self as w, co}; use winsafe::{self as w, co};
use velopack::bundle::load_bundle_from_file;
use velopack::locator::{auto_locate_app_manifest, LocationContext};
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
#[test] #[test]
@@ -37,7 +39,8 @@ pub fn test_install_apply_uninstall() {
let tmp_dir = tempdir().unwrap(); let tmp_dir = tempdir().unwrap();
let tmp_buf = tmp_dir.path().to_path_buf(); let tmp_buf = tmp_dir.path().to_path_buf();
commands::install(Some(&nupkg), Some(&tmp_buf)).unwrap(); let mut tmp_zip = load_bundle_from_file(nupkg).unwrap();
commands::install(&mut tmp_zip, Some(&tmp_buf), None).unwrap();
assert!(!lnk_desktop_1.exists()); // desktop is created during update assert!(!lnk_desktop_1.exists()); // desktop is created during update
assert!(lnk_start_1.exists()); assert!(lnk_start_1.exists());
@@ -46,13 +49,13 @@ pub fn test_install_apply_uninstall() {
assert!(tmp_buf.join("current").join("AvaloniaCrossPlat.exe").exists()); assert!(tmp_buf.join("current").join("AvaloniaCrossPlat.exe").exists());
assert!(tmp_buf.join("current").join("sq.version").exists()); assert!(tmp_buf.join("current").join("sq.version").exists());
let (root_dir, app) = shared::detect_manifest_from_update_path(&tmp_buf.join("Update.exe")).unwrap(); let locator = auto_locate_app_manifest(LocationContext::FromSpecifiedRootDir(tmp_buf.clone())).unwrap();
assert_eq!(app_id, app.id); assert_eq!(app_id, locator.get_manifest_id());
assert_eq!(semver::Version::parse("1.0.11").unwrap(), app.version); assert_eq!(semver::Version::parse("1.0.11").unwrap(), locator.get_manifest_version());
let pkg_name_apply = "AvaloniaCrossPlat-1.0.15-win-full.nupkg"; let pkg_name_apply = "AvaloniaCrossPlat-1.0.15-win-full.nupkg";
let nupkg_apply = fixtures.join(pkg_name_apply); let nupkg_apply = fixtures.join(pkg_name_apply);
commands::apply(&root_dir, &app, false, shared::OperationWait::NoWait, Some(&nupkg_apply), None, false).unwrap(); commands::apply(&locator, false, shared::OperationWait::NoWait, Some(&nupkg_apply), None, false).unwrap();
// shortcuts are renamed, and desktop is created // shortcuts are renamed, and desktop is created
assert!(!lnk_desktop_1.exists()); assert!(!lnk_desktop_1.exists());
@@ -60,10 +63,10 @@ pub fn test_install_apply_uninstall() {
assert!(lnk_desktop_2.exists()); assert!(lnk_desktop_2.exists());
assert!(lnk_start_2.exists()); assert!(lnk_start_2.exists());
let (root_dir, app) = shared::detect_manifest_from_update_path(&tmp_buf.join("Update.exe")).unwrap(); let locator = auto_locate_app_manifest(LocationContext::FromSpecifiedRootDir(tmp_buf.clone())).unwrap();
assert_eq!(semver::Version::parse("1.0.15").unwrap(), app.version); assert_eq!(semver::Version::parse("1.0.15").unwrap(), locator.get_manifest_version());
commands::uninstall(&root_dir, &app, false).unwrap(); commands::uninstall(&locator, false).unwrap();
assert!(!tmp_buf.join("current").exists()); assert!(!tmp_buf.join("current").exists());
assert!(tmp_buf.join(".dead").exists()); assert!(tmp_buf.join(".dead").exists());
@@ -83,7 +86,9 @@ pub fn test_install_preserve_symlinks() {
let tmp_dir = tempdir().unwrap(); let tmp_dir = tempdir().unwrap();
let tmp_buf = tmp_dir.path().to_path_buf(); let tmp_buf = tmp_dir.path().to_path_buf();
commands::install(Some(&nupkg), Some(&tmp_buf)).unwrap(); let mut tmp_zip = load_bundle_from_file(nupkg).unwrap();
commands::install(&mut tmp_zip, Some(&tmp_buf), None).unwrap();
assert!(tmp_buf.join("current").join("actual").join("file.txt").exists()); assert!(tmp_buf.join("current").join("actual").join("file.txt").exists());
assert!(tmp_buf.join("current").join("other").join("syml").exists()); assert!(tmp_buf.join("current").join("other").join("syml").exists());
@@ -112,14 +117,14 @@ pub fn test_patch_apply() {
} }
let expected_sha1 = get_sha1(&new_file); let expected_sha1 = get_sha1(&new_file);
let tmp_file = std::path::Path::new("temp.patch").to_path_buf(); let tmp_file = Path::new("temp.patch").to_path_buf();
commands::patch(&old_file, &p1, &tmp_file).unwrap(); velopack::delta::zstd_patch_single(&old_file, &p1, &tmp_file).unwrap();
let tmp_sha1 = get_sha1(&tmp_file); let tmp_sha1 = get_sha1(&tmp_file);
fs::remove_file(&tmp_file).unwrap(); fs::remove_file(&tmp_file).unwrap();
assert_eq!(expected_sha1, tmp_sha1); assert_eq!(expected_sha1, tmp_sha1);
commands::patch(&old_file, &p2, &tmp_file).unwrap(); velopack::delta::zstd_patch_single(&old_file, &p2, &tmp_file).unwrap();
let tmp_sha1 = get_sha1(&tmp_file); let tmp_sha1 = get_sha1(&tmp_file);
fs::remove_file(&tmp_file).unwrap(); fs::remove_file(&tmp_file).unwrap();
assert_eq!(expected_sha1, tmp_sha1); assert_eq!(expected_sha1, tmp_sha1);

View File

@@ -15,8 +15,8 @@ serde_json = "1"
velopack = { path = "../../../lib-rust" } velopack = { path = "../../../lib-rust" }
semver = "1.0" semver = "1.0"
log = "0.4" log = "0.4"
lazy_static = "1.4" lazy_static = "1.5"
[build-dependencies] [build-dependencies]
ts-rs = "9" ts-rs = "10.0"
velopack = { path = "../../../lib-rust", features = ["typescript"] } velopack = { path = "../../../lib-rust", features = ["typescript"] }

View File

@@ -1,5 +1,5 @@
use std::{env, path::Path}; use std::{env, path::Path};
use locator::VelopackLocator; use locator::VelopackLocatorConfig;
use ts_rs::TS; use ts_rs::TS;
use velopack::*; use velopack::*;
@@ -8,5 +8,5 @@ fn main() {
let bindings_dir = Path::new(&manifest_dir).join("..").join("..").join("src").join("bindings"); let bindings_dir = Path::new(&manifest_dir).join("..").join("..").join("src").join("bindings");
UpdateInfo::export_all_to(&bindings_dir).unwrap(); UpdateInfo::export_all_to(&bindings_dir).unwrap();
UpdateOptions::export_all_to(&bindings_dir).unwrap(); UpdateOptions::export_all_to(&bindings_dir).unwrap();
VelopackLocator::export_all_to(&bindings_dir).unwrap(); VelopackLocatorConfig::export_all_to(&bindings_dir).unwrap();
} }

View File

@@ -16,14 +16,14 @@ struct UpdateManagerWrapper {
impl Finalize for UpdateManagerWrapper {} impl Finalize for UpdateManagerWrapper {}
type BoxedUpdateManager = JsBox<RefCell<UpdateManagerWrapper>>; type BoxedUpdateManager = JsBox<RefCell<UpdateManagerWrapper>>;
fn args_get_locator(cx: &mut FunctionContext, i: usize) -> NeonResult<Option<VelopackLocator>> { fn args_get_locator(cx: &mut FunctionContext, i: usize) -> NeonResult<Option<VelopackLocatorConfig>> {
let arg_locator = cx.argument_opt(i); let arg_locator = cx.argument_opt(i);
if let Some(js_value) = arg_locator { if let Some(js_value) = arg_locator {
if js_value.is_a::<JsString, _>(cx) { if js_value.is_a::<JsString, _>(cx) {
if let Ok(js_string) = js_value.downcast::<JsString, _>(cx) { if let Ok(js_string) = js_value.downcast::<JsString, _>(cx) {
let arg_locator = js_string.value(cx); let arg_locator = js_string.value(cx);
if !arg_locator.is_empty() { if !arg_locator.is_empty() {
let locator = serde_json::from_str::<VelopackLocator>(&arg_locator).or_else(|e| cx.throw_error(e.to_string()))?; let locator = serde_json::from_str::<VelopackLocatorConfig>(&arg_locator).or_else(|e| cx.throw_error(e.to_string()))?;
return Ok(Some(locator)); return Ok(Some(locator));
} }
} }
@@ -205,6 +205,7 @@ fn js_appbuilder_run(mut cx: FunctionContext) -> JsResult<JsUndefined> {
let undefined = cx.undefined(); let undefined = cx.undefined();
let cx_ref = Rc::new(RefCell::new(cx)); let cx_ref = Rc::new(RefCell::new(cx));
let cx_ref2 = cx_ref.clone();
let hook_handler = move |hook_name: &str, current_version: Version| { let hook_handler = move |hook_name: &str, current_version: Version| {
let mut cx = cx_ref.borrow_mut(); let mut cx = cx_ref.borrow_mut();
@@ -231,7 +232,10 @@ fn js_appbuilder_run(mut cx: FunctionContext) -> JsResult<JsUndefined> {
builder = builder.set_args(argarray); builder = builder.set_args(argarray);
} }
builder.run(); builder.run().or_else(|e| {
let mut cx = cx_ref2.borrow_mut();
cx.throw_error(e.to_string())
})?;
Ok(undefined) Ok(undefined)
} }

View File

@@ -0,0 +1,30 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* VelopackLocator provides some utility functions for locating the current app important paths (eg. path to packages, update binary, and so forth).
*/
export type VelopackLocatorConfig = {
/**
* The root directory of the current app.
*/
RootAppDir: string,
/**
* The path to the Update.exe binary.
*/
UpdateExePath: string,
/**
* The path to the packages' directory.
*/
PackagesDir: string,
/**
* The current app manifest.
*/
ManifestPath: string,
/**
* The directory containing the application's user binaries.
*/
CurrentBinaryDir: string,
/**
* Whether the current application is portable or installed.
*/
IsPortable: boolean, };

View File

@@ -3,7 +3,7 @@
/** /**
* VelopackLocator provides some utility functions for locating the current app important paths (eg. path to packages, update binary, and so forth). * VelopackLocator provides some utility functions for locating the current app important paths (eg. path to packages, update binary, and so forth).
*/ */
export type VelopackLocator = { export type VelopackPaths = {
/** /**
* The root directory of the current app. * The root directory of the current app.
*/ */
@@ -21,6 +21,6 @@ PackagesDir: string,
*/ */
ManifestPath: string, ManifestPath: string,
/** /**
* The temporary directory for the current app. * The directory containing the application's user binaries
*/ */
TempDir: string, }; CurrentBinaryDir: string, };

View File

@@ -32,9 +32,13 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0" } serde_json = { version = "1.0" }
zip = { version = "2.1", default-features = false, features = ["deflate"] } zip = { version = "2.1", default-features = false, features = ["deflate"] }
thiserror = "1.0" thiserror = "1.0"
lazy_static = "1.5"
regex = "1.10"
normpath = "1.2"
bitflags = "2.6"
# typescript # typescript
ts-rs = { version = "9", optional = true } ts-rs = { version = "10.0", optional = true }
# delta packages # delta packages
zstd = { version = "0.13", optional = true } zstd = { version = "0.13", optional = true }

View File

@@ -3,9 +3,10 @@ use std::env;
use std::process::exit; use std::process::exit;
use crate::{ use crate::{
locator::{auto_locate, VelopackLocator}, locator::{auto_locate_app_manifest, VelopackLocatorConfig},
Error, Error,
}; };
use crate::locator::{LocationContext, VelopackLocator};
/// VelopackApp helps you to handle app activation events correctly. /// VelopackApp helps you to handle app activation events correctly.
/// This should be used as early as possible in your application startup code. /// This should be used as early as possible in your application startup code.
@@ -19,7 +20,7 @@ pub struct VelopackApp<'a> {
restarted_hook: Option<Box<dyn FnOnce(Version) + 'a>>, restarted_hook: Option<Box<dyn FnOnce(Version) + 'a>>,
// auto_apply: bool, // auto_apply: bool,
args: Vec<String>, args: Vec<String>,
locator: Option<VelopackLocator>, locator: Option<VelopackLocatorConfig>,
} }
impl<'a> VelopackApp<'a> { impl<'a> VelopackApp<'a> {
@@ -51,7 +52,7 @@ impl<'a> VelopackApp<'a> {
// } // }
/// Override the default file locator with a custom one (eg. for testing) /// Override the default file locator with a custom one (eg. for testing)
pub fn set_locator(mut self, locator: VelopackLocator) -> Self { pub fn set_locator(mut self, locator: VelopackLocatorConfig) -> Self {
self.locator = Some(locator); self.locator = Some(locator);
self self
} }
@@ -110,7 +111,7 @@ impl<'a> VelopackApp<'a> {
/// Runs the Velopack startup logic. This should be the first thing to run in your app. /// Runs the Velopack startup logic. This should be the first thing to run in your app.
/// In some circumstances it may terminate/restart the process to perform tasks. /// In some circumstances it may terminate/restart the process to perform tasks.
pub fn run(&mut self) { pub fn run(&mut self) -> Result<(), Error> {
let args: Vec<String> = self.args.clone(); let args: Vec<String> = self.args.clone();
info!("VelopackApp: Running with args: {:?}", args); info!("VelopackApp: Running with args: {:?}", args);
@@ -124,14 +125,15 @@ impl<'a> VelopackApp<'a> {
_ => {} // Handle other cases or do nothing _ => {} // Handle other cases or do nothing
} }
} }
let my_version = match self.get_current_version() { let locator = if let Some(config) = &self.locator {
Ok(ver) => ver, let manifest = config.load_manifest()?;
Err(e) => { VelopackLocator::new(config.clone(), manifest)
warn!("VelopackApp: Error getting current version: {}", e); } else {
semver::Version::new(0, 0, 0) auto_locate_app_manifest(LocationContext::FromCurrentExe)?
}
}; };
let my_version = locator.get_manifest_version();
let firstrun = env::var("VELOPACK_FIRSTRUN").is_ok(); let firstrun = env::var("VELOPACK_FIRSTRUN").is_ok();
let restarted = env::var("VELOPACK_RESTART").is_ok(); let restarted = env::var("VELOPACK_RESTART").is_ok();
@@ -145,6 +147,8 @@ impl<'a> VelopackApp<'a> {
if restarted { if restarted {
Self::call_hook(&mut self.restarted_hook, &my_version); Self::call_hook(&mut self.restarted_hook, &my_version);
} }
Ok(())
} }
fn call_hook(hook_option: &mut Option<Box<dyn FnOnce(Version) + 'a>>, version: &Version) { fn call_hook(hook_option: &mut Option<Box<dyn FnOnce(Version) + 'a>>, version: &Version) {
@@ -164,17 +168,4 @@ impl<'a> VelopackApp<'a> {
} }
} }
} }
fn get_locator(&self) -> Result<VelopackLocator, Error> {
if let Some(locator) = &self.locator {
return Ok(locator.clone());
}
let exe_path = env::current_exe()?;
auto_locate(exe_path)
}
fn get_current_version(&self) -> Result<Version, Error> {
self.get_locator().map(|l| l.load_manifest()).map(|m| m.map(|m| m.version))?
}
} }

View File

@@ -1,3 +1,5 @@
#![allow(missing_docs)]
use std::{ use std::{
cell::RefCell, cell::RefCell,
fs::{self, File}, fs::{self, File},
@@ -5,10 +7,18 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
rc::Rc, rc::Rc,
}; };
use std::io::Cursor;
use regex::Regex;
use semver::Version;
use xml::EventReader;
use xml::reader::XmlEvent;
use zip::ZipArchive; use zip::ZipArchive;
use crate::{Error, manifest::*, util}; use crate::{Error, util};
#[cfg(target_os = "windows")]
use normpath::PathExt;
pub trait ReadSeek: Read + Seek {} pub trait ReadSeek: Read + Seek {}
@@ -17,20 +27,52 @@ impl<T: Read + Seek> ReadSeek for T {}
#[derive(Clone)] #[derive(Clone)]
pub struct BundleZip<'a> { pub struct BundleZip<'a> {
zip: Rc<RefCell<ZipArchive<Box<dyn ReadSeek + 'a>>>>, zip: Rc<RefCell<ZipArchive<Box<dyn ReadSeek + 'a>>>>,
zip_from_file: bool,
zip_range: Option<&'a [u8]>,
file_path: Option<PathBuf>,
manifest: Option<Manifest>,
} }
#[allow(dead_code)]
pub fn load_bundle_from_file<'a, P: AsRef<Path>>(file_name: P) -> Result<BundleZip<'a>, Error> { pub fn load_bundle_from_file<'a, P: AsRef<Path>>(file_name: P) -> Result<BundleZip<'a>, Error> {
let file_name = file_name.as_ref(); let file_name = file_name.as_ref();
debug!("Loading bundle from file '{}'...", file_name.to_string_lossy()); debug!("Loading bundle from file '{}'...", file_name.to_string_lossy());
let file = util::retry_io(|| File::open(&file_name))?; let file = util::retry_io(|| File::open(&file_name))?;
let cursor: Box<dyn ReadSeek> = Box::new(file); let cursor: Box<dyn ReadSeek> = Box::new(file);
let zip = ZipArchive::new(cursor)?; let zip = ZipArchive::new(cursor)?;
Ok(BundleZip { zip: Rc::new(RefCell::new(zip)) }) Ok(BundleZip {
zip: Rc::new(RefCell::new(zip)),
zip_from_file: true,
file_path: Some(file_name.to_owned()),
zip_range: None,
manifest: None,
})
}
pub fn load_bundle_from_memory(zip_range: &[u8]) -> Result<BundleZip, Error> {
info!("Loading bundle from embedded zip...");
let cursor: Box<dyn ReadSeek> = Box::new(Cursor::new(zip_range));
let zip = ZipArchive::new(cursor)?;
Ok(BundleZip {
zip: Rc::new(RefCell::new(zip)),
zip_from_file: false,
zip_range: Some(zip_range),
file_path: None,
manifest: None,
})
} }
#[allow(dead_code)] #[allow(dead_code)]
impl BundleZip<'_> { impl BundleZip<'_> {
pub fn copy_bundle_to_file<T: AsRef<Path>>(&self, output_file_path: T) -> Result<(), Error> {
let nupkg_path = output_file_path.as_ref();
if self.zip_from_file {
util::retry_io(|| fs::copy(self.file_path.clone().unwrap(), nupkg_path))?;
} else {
util::retry_io(|| fs::write(nupkg_path, self.zip_range.unwrap()))?;
}
Ok(())
}
pub fn calculate_size(&self) -> (u64, u64) { pub fn calculate_size(&self) -> (u64, u64) {
let mut total_uncompressed_size = 0u64; let mut total_uncompressed_size = 0u64;
let mut total_compressed_size = 0u64; let mut total_compressed_size = 0u64;
@@ -78,7 +120,8 @@ impl BundleZip<'_> {
} }
pub fn find_zip_file<F>(&self, predicate: F) -> Option<usize> pub fn find_zip_file<F>(&self, predicate: F) -> Option<usize>
where F: Fn(&str) -> bool, where
F: Fn(&str) -> bool,
{ {
let mut archive = self.zip.borrow_mut(); let mut archive = self.zip.borrow_mut();
for i in 0..archive.len() { for i in 0..archive.len() {
@@ -121,7 +164,8 @@ impl BundleZip<'_> {
} }
pub fn extract_zip_predicate_to_path<F, T: AsRef<Path>>(&self, predicate: F, path: T) -> Result<usize, Error> pub fn extract_zip_predicate_to_path<F, T: AsRef<Path>>(&self, predicate: F, path: T) -> Result<usize, Error>
where F: Fn(&str) -> bool, where
F: Fn(&str) -> bool,
{ {
let idx = self.find_zip_file(predicate); let idx = self.find_zip_file(predicate);
if idx.is_none() { if idx.is_none() {
@@ -132,7 +176,11 @@ impl BundleZip<'_> {
Ok(idx) Ok(idx)
} }
pub fn read_manifest(&self) -> Result<Manifest, Error> { pub fn read_manifest(&mut self) -> Result<Manifest, Error> {
if let Some(manifest) = &self.manifest {
return Ok(manifest.clone());
}
let nuspec_idx = self.find_zip_file(|name| name.ends_with(".nuspec")) let nuspec_idx = self.find_zip_file(|name| name.ends_with(".nuspec"))
.ok_or(Error::MissingNuspec)?; .ok_or(Error::MissingNuspec)?;
@@ -140,6 +188,8 @@ impl BundleZip<'_> {
let mut archive = self.zip.borrow_mut(); let mut archive = self.zip.borrow_mut();
archive.by_index(nuspec_idx)?.read_to_string(&mut contents)?; archive.by_index(nuspec_idx)?.read_to_string(&mut contents)?;
let app = read_manifest_from_string(&contents)?; let app = read_manifest_from_string(&contents)?;
self.manifest = Some(app.clone());
Ok(app) Ok(app)
} }
@@ -158,4 +208,317 @@ impl BundleZip<'_> {
} }
Ok(files) Ok(files)
} }
#[cfg(not(target_os = "linux"))]
fn create_symlink(link_path: &PathBuf, target_path: &PathBuf) -> Result<(), Error> {
#[cfg(target_os = "windows")]
{
let absolute_path = link_path.parent().unwrap().join(&target_path);
trace!(
"Creating symlink '{}' -> '{}', target isfile={}, isdir={}, relative={}",
link_path.to_string_lossy(),
absolute_path.to_string_lossy(),
absolute_path.is_file(),
absolute_path.is_dir(),
target_path.to_string_lossy()
);
if absolute_path.is_file() {
std::os::windows::fs::symlink_file(target_path, link_path)?;
} else if absolute_path.is_dir() {
std::os::windows::fs::symlink_dir(target_path, link_path)?;
} else {
return Err(Error::Generic("Could not create symlink: target is not a file or directory.".to_owned()));
}
}
#[cfg(not(target_os = "windows"))]
{
std::os::unix::fs::symlink(target_path, link_path)?;
}
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub fn extract_lib_contents_to_path<P: AsRef<Path>, F: Fn(i16)>(&self, current_path: P, progress: F) -> Result<(), Error> {
let current_path = current_path.as_ref();
let files = self.get_file_names()?;
let num_files = files.len();
info!("Extracting {} app files to '{}'...", num_files, current_path.to_string_lossy());
let re = Regex::new(r"lib[\\\/][^\\\/]*[\\\/]").unwrap();
let stub_regex = Regex::new("_ExecutionStub.exe$").unwrap();
let symlink_regex = Regex::new(".__symlink$").unwrap();
let updater_idx = self.find_zip_file(|name| name.ends_with("Squirrel.exe"));
// for legacy support, we still extract the nuspec file to the current dir.
// in newer versions, the nuspec is in the current dir in the package itself.
#[cfg(target_os = "windows")]
{
let nuspec_path = current_path.join("sq.version");
let _ = self
.extract_zip_predicate_to_path(|name| name.ends_with(".nuspec"), nuspec_path)
.map_err(|_| Error::MissingNuspec)?;
}
// we extract the symlinks after, because the target must exist.
let mut symlinks: Vec<(usize, PathBuf)> = Vec::new();
for (i, key) in files.iter().enumerate() {
if Some(i) == updater_idx || !re.is_match(key) || key.ends_with("/") || key.ends_with("\\") {
debug!(" {} Skipped '{}'", i, key);
continue;
}
let file_path_in_zip = re.replace(key, "").to_string();
let file_path_on_disk = Path::new(&current_path).join(&file_path_in_zip);
if symlink_regex.is_match(&file_path_in_zip) {
let sym_key = symlink_regex.replace(&file_path_in_zip, "").to_string();
let file_path_on_disk = Path::new(&current_path).join(&sym_key);
symlinks.push((i, file_path_on_disk));
continue;
}
if stub_regex.is_match(&file_path_in_zip) {
// let stub_key = stub_regex.replace(&file_path_in_zip, ".exe").to_string();
// file_path_on_disk = root_path.join(&stub_key);
debug!(" {} Skipped Stub (obsolete) '{}'", i, key);
continue;
}
// on windows, the zip paths are / and should be \ instead
#[cfg(target_os = "windows")]
let file_path_on_disk = file_path_on_disk.normalize_virtually()?;
#[cfg(target_os = "windows")]
let file_path_on_disk = file_path_on_disk.as_path();
debug!(" {} Extracting '{}' to '{}'", i, key, file_path_on_disk.to_string_lossy());
self.extract_zip_idx_to_path(i, &file_path_on_disk)?;
// on macos, we need to chmod +x the executable files
#[cfg(target_os = "macos")]
{
if let Ok(true) = super::macho::is_macho_image(&file_path_on_disk) {
if let Err(e) = std::fs::set_permissions(&file_path_on_disk, std::fs::Permissions::from_mode(0o755)) {
warn!("Failed to set executable permissions on '{}': {}", file_path_on_disk.to_string_lossy(), e);
} else {
info!(" {} Set executable permissions on '{}'", i, file_path_on_disk.to_string_lossy());
}
}
}
progress(((i as f32 / num_files as f32) * 100.0) as i16);
}
// we extract the symlinks after, because the target must exist.
for (i, link_path) in symlinks {
let mut archive = self.zip.borrow_mut();
let mut file = archive.by_index(i)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
info!(" {} Creating symlink '{}' -> '{}'", i, link_path.to_string_lossy(), contents);
let contents = contents.trim_end_matches('/');
#[cfg(target_os = "windows")]
let contents = contents.replace("/", "\\");
let contents = PathBuf::from(contents);
let parent = link_path.parent().unwrap();
if !parent.exists() {
debug!("Creating parent directory: {:?}", parent);
util::retry_io(|| fs::create_dir_all(parent))?;
}
util::retry_io(|| Self::create_symlink(&link_path, &contents))?;
}
Ok(())
}
} }
#[derive(Debug, derivative::Derivative, Clone)]
#[derivative(Default)]
#[allow(missing_docs)]
pub struct Manifest {
pub id: String,
#[derivative(Default(value = "Version::new(0, 0, 0)"))]
pub version: Version,
pub title: String,
pub authors: String,
pub description: String,
pub machine_architecture: String,
pub runtime_dependencies: String,
pub main_exe: String,
pub os: String,
pub os_min_version: String,
pub channel: String,
pub shortcut_locations: String,
pub shortcut_amuid: String,
}
/// Parse manifest object from an XML string.
pub fn read_manifest_from_string(xml: &str) -> Result<Manifest, Error> {
let mut obj: Manifest = Default::default();
let cursor = Cursor::new(xml);
let parser = EventReader::new(cursor);
let mut vec: Vec<String> = Vec::new();
for e in parser {
match e {
Ok(XmlEvent::StartElement { name, .. }) => {
vec.push(name.local_name);
}
Ok(XmlEvent::Characters(text)) => {
if vec.is_empty() {
continue;
}
let el_name = vec.last().unwrap();
if el_name == "id" {
obj.id = text;
} else if el_name == "version" {
obj.version = Version::parse(&text)?;
} else if el_name == "title" {
obj.title = text;
} else if el_name == "authors" {
obj.authors = text;
} else if el_name == "description" {
obj.description = text;
} else if el_name == "machineArchitecture" {
obj.machine_architecture = text;
} else if el_name == "runtimeDependencies" {
obj.runtime_dependencies = text;
} else if el_name == "mainExe" {
obj.main_exe = text;
} else if el_name == "os" {
obj.os = text;
} else if el_name == "osMinVersion" {
obj.os_min_version = text;
} else if el_name == "channel" {
obj.channel = text;
} else if el_name == "shortcutLocations" {
obj.shortcut_locations = text;
} else if el_name == "shortcutAmuid" {
obj.shortcut_amuid = text;
}
}
Ok(XmlEvent::EndElement { .. }) => {
vec.pop();
}
Err(e) => {
error!("Error: {e}");
break;
}
// There's more: https://docs.rs/xml-rs/latest/xml/reader/enum.XmlEvent.html
_ => {}
}
}
if obj.id.is_empty() {
return Err(Error::MissingNuspecProperty("id".to_owned()));
}
if obj.version == Version::new(0, 0, 0) {
return Err(Error::MissingNuspecProperty("version".to_owned()));
}
#[cfg(target_os = "windows")]
if obj.main_exe.is_empty() {
return Err(Error::MissingNuspecProperty("mainExe".to_owned()));
}
if obj.title.is_empty() {
obj.title = obj.id.clone();
}
Ok(obj)
}
#[derive(Debug, Clone, derivative::Derivative)]
#[derivative(Default)]
pub struct EntryNameInfo {
pub name: String,
#[derivative(Default(value = "Version::new(0, 0, 0)"))]
pub version: Version,
pub is_delta: bool,
pub file_path: String,
}
impl EntryNameInfo {
pub fn load_manifest(&self) -> Result<Manifest, Error> {
let path = Path::new(&self.file_path).to_path_buf();
let mut bundle = load_bundle_from_file(&path)?;
bundle.read_manifest()
}
}
lazy_static::lazy_static! {
static ref ENTRY_SUFFIX_FULL: Regex = Regex::new(r"(?i)-full.nupkg$").unwrap();
static ref ENTRY_SUFFIX_DELTA: Regex = Regex::new(r"(?i)-delta.nupkg$").unwrap();
static ref ENTRY_VERSION_START: Regex = Regex::new(r"[\.-](0|[1-9]\d*)\.(0|[1-9]\d*)($|[^\d])").unwrap();
}
/// Parse a package file path into an EntryNameInfo object. Returns None if couldn't be parsed.
pub fn parse_package_file_path<P: AsRef<Path>>(path: P) -> Option<EntryNameInfo> {
let path = path.as_ref();
let name = path.file_name()?.to_string_lossy().to_string();
let m = parse_package_file_name(name);
if m.is_some() {
let mut m = m.unwrap();
m.file_path = path.to_string_lossy().to_string();
return Some(m);
}
m
}
fn parse_package_file_name<T: AsRef<str>>(name: T) -> Option<EntryNameInfo> {
let name = name.as_ref();
let full = ENTRY_SUFFIX_FULL.is_match(name);
let delta = ENTRY_SUFFIX_DELTA.is_match(name);
if !full && !delta {
return None;
}
let mut entry = EntryNameInfo::default();
entry.is_delta = delta;
let name_and_ver = if full { ENTRY_SUFFIX_FULL.replace(name, "") } else { ENTRY_SUFFIX_DELTA.replace(name, "") };
let ver_idx = ENTRY_VERSION_START.find(&name_and_ver);
if ver_idx.is_none() {
return None;
}
let ver_idx = ver_idx.unwrap().start();
entry.name = name_and_ver[0..ver_idx].to_string();
let ver_idx = ver_idx + 1;
let version = name_and_ver[ver_idx..].to_string();
let sv = Version::parse(&version);
if sv.is_err() {
return None;
}
entry.version = sv.unwrap();
return Some(entry);
}
#[test]
fn test_parse_package_file_name() {
// test no rid
let entry = parse_package_file_name("Velopack-1.0.0-full.nupkg").unwrap();
assert_eq!(entry.name, "Velopack");
assert_eq!(entry.version, Version::parse("1.0.0").unwrap());
assert_eq!(entry.is_delta, false);
let entry = parse_package_file_name("Velopack-1.0.0-delta.nupkg").unwrap();
assert_eq!(entry.name, "Velopack");
assert_eq!(entry.version, Version::parse("1.0.0").unwrap());
assert_eq!(entry.is_delta, true);
let entry = parse_package_file_name("My.Cool-App-1.1.0-full.nupkg").unwrap();
assert_eq!(entry.name, "My.Cool-App");
assert_eq!(entry.version, Version::parse("1.1.0").unwrap());
assert_eq!(entry.is_delta, false);
// test invalid names
assert!(parse_package_file_name("MyCoolApp-1.2.3-beta1-win7-x64-full.nupkg.zip").is_none());
assert!(parse_package_file_name("MyCoolApp-1.2.3-beta1-win7-x64-full.zip").is_none());
assert!(parse_package_file_name("MyCoolApp-1.2.3.nupkg").is_none());
assert!(parse_package_file_name("MyCoolApp-1.2-full.nupkg").is_none());
}

View File

@@ -3,6 +3,7 @@ use std::io::{Read, Write};
use crate::{Error, util}; use crate::{Error, util};
/// Downloads a file from a URL and writes it to a file while reporting progress from 0-100.
pub fn download_url_to_file<A>(url: &str, file_path: &str, mut progress: A) -> Result<(), Error> pub fn download_url_to_file<A>(url: &str, file_path: &str, mut progress: A) -> Result<(), Error>
where A: FnMut(i16), where A: FnMut(i16),
{ {
@@ -39,6 +40,7 @@ pub fn download_url_to_file<A>(url: &str, file_path: &str, mut progress: A) -> R
Ok(()) Ok(())
} }
/// Downloads a file from a URL and returns it as a string.
pub fn download_url_as_string(url: &str) -> Result<String, Error> { pub fn download_url_as_string(url: &str) -> Result<String, Error> {
let agent = get_download_agent()?; let agent = get_download_agent()?;
let r = agent.get(url).call()?.into_string()?; let r = agent.get(url).call()?.into_string()?;

View File

@@ -43,16 +43,14 @@
//! //!
//! fn update_my_app() { //! fn update_my_app() {
//! let source = sources::HttpSource::new("https://the.place/you-host/updates"); //! let source = sources::HttpSource::new("https://the.place/you-host/updates");
//! let um = UpdateManager::new(source, None).unwrap(); //! let um = UpdateManager::new(source, None, None).unwrap();
//! //!
//! if let UpdateCheck::UpdateAvailable(updates) = um.check_for_updates().unwrap() { //! if let UpdateCheck::UpdateAvailable(updates) = um.check_for_updates().unwrap() {
//! // there was an update available. Download it. //! // there was an update available. Download it.
//! um.download_updates(&updates, |progress| { //! um.download_updates(&updates, None).unwrap();
//! println!("Download progress: {}%", progress);
//! }).unwrap();
//! //!
//! // download completed, let's restart and update //! // download completed, let's restart and update
//! um.apply_updates_and_restart(&updates, RestartArgs::None).unwrap(); //! um.apply_updates_and_restart(&updates).unwrap();
//! } //! }
//! } //! }
//! ``` //! ```
@@ -66,7 +64,7 @@
//! ```sh //! ```sh
//! dotnet tool update -g vpk //! dotnet tool update -g vpk
//! ``` //! ```
//! ***Note: you must have the .NET Core SDK 6 installed to use and update `vpk`*** //! ***Note: you must have the .NET Core SDK 8 installed to use and update `vpk`***
//! //!
//! 6. Package your Velopack release / installers: //! 6. Package your Velopack release / installers:
//! ```sh //! ```sh
@@ -81,12 +79,15 @@
#![warn(missing_docs)] #![warn(missing_docs)]
mod app; mod app;
mod bundle;
mod download;
mod manager; mod manager;
mod manifest;
mod util; mod util;
/// Utility functions for loading and working with Velopack bundles and manifests.
pub mod bundle;
/// Utility function for downloading files with progress reporting.
pub mod download;
/// Locator provides some utility functions for locating the current app important paths (eg. path to packages, update binary, and so forth). /// Locator provides some utility functions for locating the current app important paths (eg. path to packages, update binary, and so forth).
pub mod locator; pub mod locator;
@@ -138,6 +139,8 @@ pub enum Error
MissingUpdateExe, MissingUpdateExe,
#[error("This application is not properly installed: {0}")] #[error("This application is not properly installed: {0}")]
NotInstalled(String), NotInstalled(String),
#[error("Generic error: {0}")]
Generic(String),
} }
impl From<url::ParseError> for Error { impl From<url::ParseError> for Error {

View File

@@ -1,7 +1,7 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use semver::Version;
use crate::{ use crate::{
manifest::{self, Manifest}, bundle::{self, Manifest},
util, Error, util, Error,
}; };
@@ -16,15 +16,15 @@ pub fn default_channel_name() -> String {
} }
/// Default log location for Velopack on the current OS. /// Default log location for Velopack on the current OS.
pub fn default_log_location() -> PathBuf { pub fn default_log_location(context: LocationContext) -> PathBuf {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
let mut my_exe = std::env::current_exe().expect("Could not locate current executable"); if let Ok(locator) = auto_locate_app_manifest(context) {
if let Ok(locator) = auto_locate(&my_exe) { return locator.get_root_dir().join("Velopack.log");
return locator.RootAppDir.join("Velopack.log");
} }
// If we can't locate the current app, we write to the parent directory. // If we can't locate the current app, we write to the parent directory.
let mut my_exe = std::env::current_exe().expect("Could not locate current executable");
my_exe.pop(); my_exe.pop();
my_exe.pop(); my_exe.pop();
return my_exe.join("Velopack.log"); return my_exe.join("Velopack.log");
@@ -44,74 +44,354 @@ pub fn default_log_location() -> PathBuf {
} }
} }
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
/// ShortcutLocationFlags is a bitflags enumeration of system shortcut locations.
pub struct ShortcutLocationFlags: u32 {
/// No shortcut.
const NONE = 0;
/// Start Menu shortcut inside a PackAuthor folder.
const START_MENU = 1 << 0;
/// Desktop shortcut.
const DESKTOP = 1 << 1;
/// Startup shortcut.
const STARTUP = 1 << 2;
//const APP_ROOT = 1 << 3,
/// Start Menu shortcut at the root level (not inside an author/publisher folder).
const START_MENU_ROOT = 1 << 4;
/// User pinned to taskbar shortcut.
const USER_PINNED = 1 << 5;
}
}
impl ShortcutLocationFlags {
/// Parses a string containing comma or semicolon delimited shortcut flags.
pub fn from_string(input: &str) -> ShortcutLocationFlags {
let mut flags = ShortcutLocationFlags::NONE;
for part in input.split(|c| c == ',' || c == ';') {
match part.trim().to_lowercase().as_str() {
"none" => flags |= ShortcutLocationFlags::NONE,
"startmenu" => flags |= ShortcutLocationFlags::START_MENU,
"desktop" => flags |= ShortcutLocationFlags::DESKTOP,
"startup" => flags |= ShortcutLocationFlags::STARTUP,
"startmenuroot" => flags |= ShortcutLocationFlags::START_MENU_ROOT,
_ => warn!("Warning: Unrecognized shortcut flag `{}`", part.trim()),
}
}
flags
}
}
/// VelopackLocator provides some utility functions for locating the current app important paths (eg. path to packages, update binary, and so forth). /// VelopackLocator provides some utility functions for locating the current app important paths (eg. path to packages, update binary, and so forth).
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Default)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct VelopackLocator { pub struct VelopackLocatorConfig {
/// The root directory of the current app. /// The root directory of the current app.
pub RootAppDir: PathBuf, pub RootAppDir: PathBuf,
/// The path to the Update.exe binary. /// The path to the Update.exe binary.
pub UpdateExePath: PathBuf, pub UpdateExePath: PathBuf,
/// The path to the packages directory. /// The path to the packages' directory.
pub PackagesDir: PathBuf, pub PackagesDir: PathBuf,
/// The current app manifest. /// The current app manifest.
pub ManifestPath: PathBuf, pub ManifestPath: PathBuf,
/// The temporary directory for the current app. /// The directory containing the application's user binaries.
pub TempDir: PathBuf, pub CurrentBinaryDir: PathBuf,
/// Whether the current application is portable or installed.
pub IsPortable: bool,
} }
impl VelopackLocator { impl VelopackLocatorConfig {
/// Load and parse the current app manifest from the manifest_path field. This will return an error if the manifest is missing. /// Load and parse the current app manifest from the manifest_path field. This will return an error if the manifest is missing.
pub fn load_manifest(&self) -> Result<Manifest, Error> { pub fn load_manifest(&self) -> Result<Manifest, Error> {
read_current_manifest(&self.ManifestPath) read_current_manifest(&self.ManifestPath)
} }
} }
#[cfg(target_os = "windows")] /// VelopackLocator provides some utility functions for locating the current app important paths
/// Automatically locates the current app's important paths. If the app is not installed, it will return an error. #[derive(Clone)]
pub fn auto_locate<P: AsRef<Path>>(exe_path: P) -> Result<VelopackLocator, Error> { pub struct VelopackLocator {
// check if Update.exe exists in parent dir, if it does, that's the root dir. paths: VelopackLocatorConfig,
let mut path = exe_path.as_ref().to_path_buf(); manifest: Manifest,
path.pop(); // current dir }
path.pop(); // root dir
if path.join("Update.exe").exists() { impl VelopackLocator {
info!("Found Update.exe in parent directory: {}", path.to_string_lossy()); /// Creates a new VelopackLocator from the given paths and manifest.
return Ok(VelopackLocator { pub fn new(paths: VelopackLocatorConfig, manifest: Manifest) -> Self {
RootAppDir: path.clone(), Self { paths, manifest }
UpdateExePath: path.join("Update.exe"),
PackagesDir: path.join("packages"),
ManifestPath: path.join("current").join("sq.version"),
TempDir: path.join("packages").join("VelopackTemp"),
});
} }
// see if we can find the current dir in the path, maybe we're more nested than that. /// Returns the path to the current app's packages directory.
path = exe_path.as_ref().to_path_buf(); pub fn get_packages_dir(&self) -> PathBuf {
let path = path.to_string_lossy(); self.paths.PackagesDir.clone()
let idx = path.rfind("\\current\\"); }
if let Some(i) = idx {
let maybe_root = &path[..i]; /// Returns the path to the current app's packages directory as a string.
let maybe_root = PathBuf::from(maybe_root); pub fn get_packages_dir_as_string(&self) -> String {
if (maybe_root.join("Update.exe")).exists() { Self::path_as_string(&self.paths.PackagesDir)
info!("Found Update.exe in parent directory: {}", maybe_root.to_string_lossy()); }
return Ok(VelopackLocator {
RootAppDir: maybe_root.clone(), /// Returns the path to the ideal local nupkg path.
UpdateExePath: maybe_root.join("Update.exe"), pub fn get_ideal_local_nupkg_path(&self, id: Option<&str>, version: Option<Version>) -> PathBuf {
PackagesDir: maybe_root.join("packages"), let id = id.unwrap_or(&self.manifest.id);
ManifestPath: maybe_root.join("current").join("sq.version"), let version = version.unwrap_or(self.manifest.version.clone());
TempDir: maybe_root.join("packages").join("VelopackTemp"), self.paths.RootAppDir.join("packages").join(format!("{}-{}-full.nupkg", id, version))
}); }
/// Returns the path to the ideal local nupkg path as a string.
pub fn get_ideal_local_nupkg_path_as_string(&self, id: Option<&str>, version: Option<Version>) -> String {
Self::path_as_string(&self.get_ideal_local_nupkg_path(id, version))
}
/// Returns the path to the current app temporary directory.
pub fn get_temp_dir(&self) -> PathBuf {
self.paths.PackagesDir.join("VelopackTemp")
}
/// Returns the path to the current app temporary directory as a string.
pub fn get_temp_dir_as_string(&self) -> String {
Self::path_as_string(&self.get_temp_dir())
}
/// Returns the root directory of the current app.
pub fn get_root_dir(&self) -> PathBuf {
self.paths.RootAppDir.clone()
}
/// Returns the root directory of the current app as a string.
pub fn get_root_dir_as_string(&self) -> String {
Self::path_as_string(&self.paths.RootAppDir)
}
/// Returns the path to the current app's Update.exe binary.
pub fn get_update_path(&self) -> PathBuf {
self.paths.UpdateExePath.clone()
}
/// Returns the path to the current app's Update.exe binary as a string.
pub fn get_update_path_as_string(&self) -> String {
Self::path_as_string(&self.paths.UpdateExePath)
}
/// Returns the path to the current app's main executable.
pub fn get_main_exe_path(&self) -> PathBuf {
self.paths.CurrentBinaryDir.join(&self.manifest.main_exe)
}
/// Returns the path to the current app's main executable as a string.
pub fn get_main_exe_path_as_string(&self) -> String {
Self::path_as_string(&self.get_main_exe_path())
}
/// Returns the path to the current app's user binary directory.
pub fn get_current_bin_dir(&self) -> PathBuf {
self.paths.CurrentBinaryDir.clone()
}
/// Returns the path to the current app's user binary directory as a string.
pub fn get_current_bin_dir_as_string(&self) -> String {
Self::path_as_string(&self.paths.CurrentBinaryDir)
}
/// Returns a clone of the current app's manifest.
pub fn get_manifest(&self) -> Manifest {
self.manifest.clone()
}
/// Returns the current app's version.
pub fn get_manifest_version(&self) -> Version {
self.manifest.version.clone()
}
/// Returns the current app's version as a string containing all parts.
pub fn get_manifest_version_full_string(&self) -> String {
self.manifest.version.to_string()
}
/// Returns the current app's version as a string in short format (eg. '1.2.3'),
/// not including any semver release groups etc.
pub fn get_manifest_version_short_string(&self) -> String {
let ver = &self.manifest.version;
format!("{}.{}.{}", ver.major, ver.minor, ver.patch)
}
/// Returns the current app package channel.
pub fn get_manifest_channel(&self) -> String {
self.manifest.channel.clone()
}
/// Returns the current app's Id.
pub fn get_manifest_id(&self) -> String {
self.manifest.id.clone()
}
/// Returns the current app's friendly / display name.
pub fn get_manifest_title(&self) -> String {
self.manifest.title.clone()
}
/// Returns the current app authors / publishers string.
pub fn get_manifest_authors(&self) -> String {
self.manifest.authors.clone()
}
/// Returns a flags enumeration of desired shortcut locations, or NONE if no shortcuts are desired.
pub fn get_manifest_shortcut_locations(&self) -> ShortcutLocationFlags {
if self.manifest.shortcut_locations.is_empty() {
return ShortcutLocationFlags::NONE;
}
if self.manifest.shortcut_locations.to_ascii_lowercase() == "none" {
return ShortcutLocationFlags::NONE;
}
ShortcutLocationFlags::from_string(&self.manifest.shortcut_locations)
}
/// Returns the desired shortcut AMUID, or None if no AMUID has been provided.
pub fn get_manifest_shortcut_amuid(&self) -> Option<String> {
if self.manifest.shortcut_amuid.is_empty() {
return None;
}
Some(self.manifest.shortcut_amuid.clone())
}
/// Returns a copy of the current VelopackLocator with the shortcut_locations
/// field set to an empty string.
pub fn clone_self_with_blank_shortcuts(&self) -> VelopackLocator
{
let mut new_manifest = self.manifest.clone();
new_manifest.shortcut_locations = "".to_string();
VelopackLocator {
paths: self.paths.clone(),
manifest: new_manifest,
} }
} }
Err(Error::MissingUpdateExe) /// Returns a copy of the current VelopackLocator with the manifest field set to the given manifest.
pub fn clone_self_with_new_manifest(&self, manifest: &Manifest) -> VelopackLocator
{
VelopackLocator {
paths: self.paths.clone(),
manifest: manifest.clone(),
}
}
/// Returns whether the app is portable or installed.
pub fn get_is_portable(&self) -> bool {
self.paths.IsPortable
}
fn path_as_string(path: &PathBuf) -> String {
path.to_string_lossy().to_string()
}
}
/// Create a paths object containing default / ideal paths for a given root directory
/// Generally, this should not be used except for installing the app for the first time.
#[cfg(target_os = "windows")]
pub fn create_config_from_root_dir<P: AsRef<Path>>(root_dir: P) -> VelopackLocatorConfig
{
let root_dir = root_dir.as_ref();
VelopackLocatorConfig {
RootAppDir: root_dir.to_path_buf(),
UpdateExePath: root_dir.join("Update.exe"),
PackagesDir: root_dir.join("packages"),
ManifestPath: root_dir.join("current").join("sq.version"),
CurrentBinaryDir: root_dir.join("current"),
IsPortable: root_dir.join(".portable").exists(),
}
}
fn config_to_locator(config: &VelopackLocatorConfig) -> Result<VelopackLocator, Error>
{
if !config.UpdateExePath.exists() {
return Err(Error::MissingUpdateExe);
}
if !config.ManifestPath.exists() {
return Err(Error::MissingNuspec);
}
let manifest = read_current_manifest(&config.ManifestPath)?;
Ok(VelopackLocator::new(config.clone(), manifest))
}
/// LocationContext is an enumeration of possible contexts for locating the current app manifest.
pub enum LocationContext
{
/// Should not really be used, will try a few other enumerations to locate the app manifest.
Unknown,
/// Locates the app manifest by assuming the current process is Update.exe.
IAmUpdateExe,
/// Locates the app manifest by assuming the current process is inside the application current/binary directory.
FromCurrentExe,
/// Locates the app manifest by assuming the app is installed in the specified root directory.
FromSpecifiedRootDir(PathBuf),
/// Locates the app manifest by assuming the specified path is inside the application current/binary directory.
FromSpecifiedAppExecutable(PathBuf),
}
#[cfg(target_os = "windows")]
/// Automatically locates the current app's important paths. If the app is not installed, it will return an error.
pub fn auto_locate_app_manifest(context: LocationContext) -> Result<VelopackLocator, Error> {
match context {
LocationContext::Unknown => {
warn!("Unknown location context, trying to auto-locate from current exe location...");
if let Ok(locator) = auto_locate_app_manifest(LocationContext::FromCurrentExe) {
return Ok(locator);
}
if let Ok(locator) = auto_locate_app_manifest(LocationContext::IAmUpdateExe) {
return Ok(locator);
}
}
LocationContext::FromCurrentExe => {
let current_exe = std::env::current_exe()?;
return auto_locate_app_manifest(LocationContext::FromSpecifiedAppExecutable(current_exe));
}
LocationContext::FromSpecifiedRootDir(root_dir) => {
let config = create_config_from_root_dir(&root_dir);
let locator = config_to_locator(&config)?;
return Ok(locator);
}
LocationContext::FromSpecifiedAppExecutable(exe_path) => {
// check if Update.exe exists in parent dir, if it does, that's the root dir.
if let Some(parent_dir) = exe_path.parent() {
if parent_dir.join("Update.exe").exists() {
info!("Found Update.exe in parent directory: {}", parent_dir.to_string_lossy());
let config = create_config_from_root_dir(&parent_dir);
let locator = config_to_locator(&config)?;
return Ok(locator);
}
}
// see if we can find the current dir in the current path, if we're more nested than that.
let path = exe_path.to_string_lossy();
let idx = path.rfind("\\current\\");
if let Some(i) = idx {
let maybe_root = &path[..i];
let maybe_root = PathBuf::from(maybe_root);
if maybe_root.join("Update.exe").exists() {
info!("Found Update.exe by current path pattern search in directory: {}", maybe_root.to_string_lossy());
let config = create_config_from_root_dir(&maybe_root);
let locator = config_to_locator(&config)?;
return Ok(locator);
}
}
}
LocationContext::IAmUpdateExe => {
let exe_path = std::env::current_exe()?;
if let Some(parent_dir) = exe_path.parent() {
let config = create_config_from_root_dir(&parent_dir);
let locator = config_to_locator(&config)?;
return Ok(locator);
}
}
};
Err(Error::NotInstalled("Could not auto-locate app manifest".to_owned()))
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
/// Automatically locates the current app's important paths. If the app is not installed, it will return an error. /// Automatically locates the current app's important paths. If the app is not installed, it will return an error.
pub fn auto_locate<P: AsRef<Path>>(exe_path: P) -> Result<VelopackLocator, Error> { pub fn auto_locate<P: AsRef<Path>>(exe_path: P) -> Result<VelopackLocatorConfig, Error> {
let path = exe_path.as_ref().to_path_buf(); let path = exe_path.as_ref().to_path_buf();
let path = path.to_string_lossy(); let path = path.to_string_lossy();
let idx = path.rfind("/usr/bin/"); let idx = path.rfind("/usr/bin/");
@@ -129,7 +409,7 @@ pub fn auto_locate<P: AsRef<Path>>(exe_path: P) -> Result<VelopackLocator, Error
} }
let app = read_current_manifest(&metadata_path)?; let app = read_current_manifest(&metadata_path)?;
Ok(VelopackLocator { Ok(VelopackLocatorConfig {
RootAppDir: root_app_dir, RootAppDir: root_app_dir,
UpdateExePath: update_exe_path, UpdateExePath: update_exe_path,
PackagesDir: PathBuf::from("/var/tmp/velopack").join(&app.id).join("packages"), PackagesDir: PathBuf::from("/var/tmp/velopack").join(&app.id).join("packages"),
@@ -140,7 +420,7 @@ pub fn auto_locate<P: AsRef<Path>>(exe_path: P) -> Result<VelopackLocator, Error
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
/// Automatically locates the current app's important paths. If the app is not installed, it will return an error. /// Automatically locates the current app's important paths. If the app is not installed, it will return an error.
pub fn auto_locate<P: AsRef<Path>>(exe_path: P) -> Result<VelopackLocator, Error> { pub fn auto_locate<P: AsRef<Path>>(exe_path: P) -> Result<VelopackLocatorConfig, Error> {
let path = exe_path.as_ref().to_path_buf(); let path = exe_path.as_ref().to_path_buf();
let path = path.to_string_lossy(); let path = path.to_string_lossy();
let idx = path.rfind(".app/"); let idx = path.rfind(".app/");
@@ -169,7 +449,7 @@ pub fn auto_locate<P: AsRef<Path>>(exe_path: P) -> Result<VelopackLocator, Error
packages_dir.push(&app.id); packages_dir.push(&app.id);
packages_dir.push("packages"); packages_dir.push("packages");
Ok(VelopackLocator { Ok(VelopackLocatorConfig {
RootAppDir: root_app_dir, RootAppDir: root_app_dir,
UpdateExePath: update_exe_path, UpdateExePath: update_exe_path,
PackagesDir: packages_dir, PackagesDir: packages_dir,
@@ -181,7 +461,7 @@ pub fn auto_locate<P: AsRef<Path>>(exe_path: P) -> Result<VelopackLocator, Error
fn read_current_manifest(nuspec_path: &PathBuf) -> Result<Manifest, Error> { fn read_current_manifest(nuspec_path: &PathBuf) -> Result<Manifest, Error> {
if nuspec_path.exists() { if nuspec_path.exists() {
if let Ok(nuspec) = util::retry_io(|| std::fs::read_to_string(&nuspec_path)) { if let Ok(nuspec) = util::retry_io(|| std::fs::read_to_string(&nuspec_path)) {
return Ok(manifest::read_manifest_from_string(&nuspec)?); return Ok(bundle::read_manifest_from_string(&nuspec)?);
} }
} }
Err(Error::MissingNuspec) Err(Error::MissingNuspec)

View File

@@ -14,11 +14,11 @@ use semver::Version;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
locator::{self, VelopackLocator}, locator::{self, VelopackLocatorConfig},
manifest::Manifest,
sources::UpdateSource, sources::UpdateSource,
Error, Error,
}; };
use crate::locator::{auto_locate_app_manifest, LocationContext, VelopackLocator};
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[derive(Serialize, Deserialize, Debug, Clone, Default)] #[derive(Serialize, Deserialize, Debug, Clone, Default)]
@@ -106,11 +106,9 @@ pub struct UpdateOptions {
/// Provides functionality for checking for updates, downloading updates, and applying updates to the current application. /// Provides functionality for checking for updates, downloading updates, and applying updates to the current application.
#[derive(Clone)] #[derive(Clone)]
pub struct UpdateManager { pub struct UpdateManager {
allow_version_downgrade: bool, options: UpdateOptions,
explicit_channel: Option<String>,
source: Box<dyn UpdateSource>, source: Box<dyn UpdateSource>,
locator: VelopackLocator, locator: VelopackLocator,
manifest: Manifest,
} }
/// Represents the result of a call to check for updates. /// Represents the result of a call to check for updates.
@@ -136,40 +134,40 @@ impl UpdateManager {
pub fn new<T: UpdateSource>( pub fn new<T: UpdateSource>(
source: T, source: T,
options: Option<UpdateOptions>, options: Option<UpdateOptions>,
locator: Option<VelopackLocator>, locator: Option<VelopackLocatorConfig>,
) -> Result<UpdateManager, Error> { ) -> Result<UpdateManager, Error> {
let locator = if let Some(loc) = locator { loc } else { let locator = if let Some(config) = locator {
let my_exe = std::env::current_exe()?; let manifest = config.load_manifest()?;
locator::auto_locate(my_exe)? VelopackLocator::new(config.clone(), manifest)
} else {
auto_locate_app_manifest(LocationContext::FromCurrentExe)?
}; };
let manifest = locator.load_manifest()?;
Ok(UpdateManager { Ok(UpdateManager {
allow_version_downgrade: options.as_ref().map(|f| f.AllowVersionDowngrade).unwrap_or(false), options: options.unwrap_or_default(),
explicit_channel: options.as_ref().map(|f| f.ExplicitChannel.clone()).unwrap_or(None),
source: source.clone_boxed(), source: source.clone_boxed(),
locator, locator,
manifest,
}) })
} }
fn get_practical_channel(&self) -> String { fn get_practical_channel(&self) -> String {
let channel = self.explicit_channel.as_deref(); let options_channel = self.options.ExplicitChannel.as_deref();
let mut channel = channel.unwrap_or(&self.manifest.channel).to_string(); let app_channel = self.locator.get_manifest_channel();
let mut channel = options_channel.unwrap_or(&app_channel).to_string();
if channel.is_empty() { if channel.is_empty() {
channel = locator::default_channel_name(); channel = locator::default_channel_name();
} }
channel channel
} }
/// The currently installed app version when you created your release. /// The currently installed app version.
pub fn current_version(&self) -> Result<String, Error> { pub fn current_version(&self) -> Result<String, Error> {
Ok(self.manifest.version.to_string()) Ok(self.locator.get_manifest_version_full_string())
} }
/// Get a list of available remote releases from the package source. /// Get a list of available remote releases from the package source.
pub fn get_release_feed(&self) -> Result<VelopackAssetFeed, Error> { pub fn get_release_feed(&self) -> Result<VelopackAssetFeed, Error> {
let channel = self.get_practical_channel(); let channel = self.get_practical_channel();
self.source.get_release_feed(&channel, &self.manifest) self.source.get_release_feed(&channel, &self.locator.get_manifest())
} }
#[cfg(feature = "async")] #[cfg(feature = "async")]
@@ -183,13 +181,14 @@ impl UpdateManager {
/// Checks for updates, returning None if there are none available. If there are updates available, this method will return an /// Checks for updates, returning None if there are none available. If there are updates available, this method will return an
/// UpdateInfo object containing the latest available release, and any delta updates that can be applied if they are available. /// UpdateInfo object containing the latest available release, and any delta updates that can be applied if they are available.
pub fn check_for_updates(&self) -> Result<UpdateCheck, Error> { pub fn check_for_updates(&self) -> Result<UpdateCheck, Error> {
let allow_downgrade = self.allow_version_downgrade; let allow_downgrade = self.options.AllowVersionDowngrade;
let app = &self.manifest; let app_channel = self.locator.get_manifest_channel();
let app_version = self.locator.get_manifest_version();
let feed = self.get_release_feed()?; let feed = self.get_release_feed()?;
let assets = feed.Assets; let assets = feed.Assets;
let practical_channel = self.get_practical_channel(); let practical_channel = self.get_practical_channel();
let is_non_default_channel = practical_channel != app.channel; let is_non_default_channel = practical_channel != app_channel;
if assets.is_empty() { if assets.is_empty() {
return Ok(UpdateCheck::RemoteIsEmpty); return Ok(UpdateCheck::RemoteIsEmpty);
@@ -218,16 +217,16 @@ impl UpdateManager {
debug!("Latest remote release: {} ({}).", remote_asset.FileName, remote_version.to_string()); debug!("Latest remote release: {} ({}).", remote_asset.FileName, remote_version.to_string());
if remote_version > app.version { if remote_version > app_version {
info!("Found newer remote release available ({} -> {}).", app.version, remote_version); info!("Found newer remote release available ({} -> {}).", app_version, remote_version);
Ok(UpdateCheck::UpdateAvailable(UpdateInfo { TargetFullRelease: remote_asset, IsDowngrade: false })) Ok(UpdateCheck::UpdateAvailable(UpdateInfo { TargetFullRelease: remote_asset, IsDowngrade: false }))
} else if remote_version < app.version && allow_downgrade { } else if remote_version < app_version && allow_downgrade {
info!("Found older remote release available and downgrade is enabled ({} -> {}).", app.version, remote_version); info!("Found older remote release available and downgrade is enabled ({} -> {}).", app_version, remote_version);
Ok(UpdateCheck::UpdateAvailable(UpdateInfo { TargetFullRelease: remote_asset, IsDowngrade: true })) Ok(UpdateCheck::UpdateAvailable(UpdateInfo { TargetFullRelease: remote_asset, IsDowngrade: true }))
} else if remote_version == app.version && allow_downgrade && is_non_default_channel { } else if remote_version == app_version && allow_downgrade && is_non_default_channel {
info!( info!(
"Latest remote release is the same version of a different channel, and downgrade is enabled ({} -> {}).", "Latest remote release is the same version of a different channel, and downgrade is enabled ({} -> {}).",
app.version, remote_version app_version, remote_version
); );
Ok(UpdateCheck::UpdateAvailable(UpdateInfo { TargetFullRelease: remote_asset, IsDowngrade: true })) Ok(UpdateCheck::UpdateAvailable(UpdateInfo { TargetFullRelease: remote_asset, IsDowngrade: true }))
} else { } else {
@@ -252,7 +251,8 @@ impl UpdateManager {
/// packages, this method will fall back to downloading the full version of the update. /// packages, this method will fall back to downloading the full version of the update.
pub fn download_updates(&self, update: &UpdateInfo, progress: Option<Sender<i16>>) -> Result<(), Error> { pub fn download_updates(&self, update: &UpdateInfo, progress: Option<Sender<i16>>) -> Result<(), Error> {
let name = &update.TargetFullRelease.FileName; let name = &update.TargetFullRelease.FileName;
let packages_dir = &self.locator.PackagesDir; let packages_dir = &self.locator.get_packages_dir();
fs::create_dir_all(packages_dir)?; fs::create_dir_all(packages_dir)?;
let target_file = packages_dir.join(name); let target_file = packages_dir.join(name);
@@ -286,7 +286,8 @@ impl UpdateManager {
match crate::bundle::load_bundle_from_file(&target_file) { match crate::bundle::load_bundle_from_file(&target_file) {
Ok(bundle) => { Ok(bundle) => {
info!("Bundle loaded successfully."); info!("Bundle loaded successfully.");
if let Err(e) = bundle.extract_zip_predicate_to_path(|f| f.ends_with("Squirrel.exe"), &self.locator.UpdateExePath) { let update_exe_path = self.locator.get_update_path();
if let Err(e) = bundle.extract_zip_predicate_to_path(|f| f.ends_with("Squirrel.exe"), update_exe_path) {
error!("Error extracting Update.exe from bundle: {}", e); error!("Error extracting Update.exe from bundle: {}", e);
} }
} }
@@ -377,7 +378,7 @@ impl UpdateManager {
C: IntoIterator<Item=S>, C: IntoIterator<Item=S>,
{ {
let to_apply = to_apply.as_ref(); let to_apply = to_apply.as_ref();
let pkg_path = self.locator.PackagesDir.join(&to_apply.FileName); let pkg_path = self.locator.get_packages_dir().join(&to_apply.FileName);
let pkg_path_str = pkg_path.to_string_lossy(); let pkg_path_str = pkg_path.to_string_lossy();
let mut args = Vec::new(); let mut args = Vec::new();
@@ -403,9 +404,9 @@ impl UpdateManager {
} }
} }
let mut p = Process::new(&self.locator.UpdateExePath); let mut p = Process::new(&self.locator.get_update_path());
p.args(&args); p.args(&args);
p.current_dir(&self.locator.RootAppDir); p.current_dir(&self.locator.get_root_dir());
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
@@ -413,7 +414,7 @@ impl UpdateManager {
p.creation_flags(CREATE_NO_WINDOW); p.creation_flags(CREATE_NO_WINDOW);
} }
info!("About to run Update.exe: {} {:?}", self.locator.UpdateExePath.to_string_lossy(), args); info!("About to run Update.exe: {} {:?}", self.locator.get_update_path_as_string(), args);
p.spawn()?; p.spawn()?;
Ok(()) Ok(())
} }

View File

@@ -1,94 +0,0 @@
use std::io::Cursor;
use semver::Version;
use xml::reader::{EventReader, XmlEvent};
use crate::Error;
#[derive(Debug, derivative::Derivative, Clone)]
#[derivative(Default)]
pub struct Manifest {
pub id: String,
#[derivative(Default(value = "Version::new(0, 0, 0)"))]
pub version: Version,
pub title: String,
pub authors: String,
pub description: String,
pub machine_architecture: String,
pub runtime_dependencies: String,
pub main_exe: String,
pub os: String,
pub os_min_version: String,
pub channel: String,
}
pub fn read_manifest_from_string(xml: &str) -> Result<Manifest, Error> {
let mut obj: Manifest = Default::default();
let cursor = Cursor::new(xml);
let parser = EventReader::new(cursor);
let mut vec: Vec<String> = Vec::new();
for e in parser {
match e {
Ok(XmlEvent::StartElement { name, .. }) => {
vec.push(name.local_name);
}
Ok(XmlEvent::Characters(text)) => {
if vec.is_empty() {
continue;
}
let el_name = vec.last().unwrap();
if el_name == "id" {
obj.id = text;
} else if el_name == "version" {
obj.version = Version::parse(&text)?;
} else if el_name == "title" {
obj.title = text;
} else if el_name == "authors" {
obj.authors = text;
} else if el_name == "description" {
obj.description = text;
} else if el_name == "machineArchitecture" {
obj.machine_architecture = text;
} else if el_name == "runtimeDependencies" {
obj.runtime_dependencies = text;
} else if el_name == "mainExe" {
obj.main_exe = text;
} else if el_name == "os" {
obj.os = text;
} else if el_name == "osMinVersion" {
obj.os_min_version = text;
} else if el_name == "channel" {
obj.channel = text;
}
}
Ok(XmlEvent::EndElement { .. }) => {
vec.pop();
}
Err(e) => {
error!("Error: {e}");
break;
}
// There's more: https://docs.rs/xml-rs/latest/xml/reader/enum.XmlEvent.html
_ => {}
}
}
if obj.id.is_empty() {
return Err(Error::MissingNuspecProperty("id".to_owned()));
}
if obj.version == Version::new(0, 0, 0) {
return Err(Error::MissingNuspecProperty("version".to_owned()));
}
#[cfg(target_os = "windows")]
if obj.main_exe.is_empty() {
return Err(Error::MissingNuspecProperty("mainExe".to_owned()));
}
if obj.title.is_empty() {
obj.title = obj.id.clone();
}
Ok(obj)
}

View File

@@ -11,7 +11,7 @@ use crate::*;
pub trait UpdateSource: Send + Sync { pub trait UpdateSource: Send + Sync {
/// Retrieve the list of available remote releases from the package source. These releases /// Retrieve the list of available remote releases from the package source. These releases
/// can subsequently be downloaded with download_release_entry. /// can subsequently be downloaded with download_release_entry.
fn get_release_feed(&self, channel: &str, app: &manifest::Manifest) -> Result<VelopackAssetFeed, Error>; fn get_release_feed(&self, channel: &str, app: &bundle::Manifest) -> Result<VelopackAssetFeed, Error>;
/// Download the specified VelopackAsset to the provided local file path. /// Download the specified VelopackAsset to the provided local file path.
fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option<Sender<i16>>) -> Result<(), Error>; fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option<Sender<i16>>) -> Result<(), Error>;
/// Clone the source to create a new lifetime. /// Clone the source to create a new lifetime.
@@ -51,7 +51,7 @@ impl AutoSource {
} }
impl UpdateSource for AutoSource { impl UpdateSource for AutoSource {
fn get_release_feed(&self, channel: &str, app: &manifest::Manifest) -> Result<VelopackAssetFeed, Error> { fn get_release_feed(&self, channel: &str, app: &bundle::Manifest) -> Result<VelopackAssetFeed, Error> {
self.source.get_release_feed(channel, app) self.source.get_release_feed(channel, app)
} }
@@ -80,7 +80,7 @@ impl HttpSource {
} }
impl UpdateSource for HttpSource { impl UpdateSource for HttpSource {
fn get_release_feed(&self, channel: &str, app: &manifest::Manifest) -> Result<VelopackAssetFeed, Error> { fn get_release_feed(&self, channel: &str, app: &bundle::Manifest) -> Result<VelopackAssetFeed, Error> {
let releases_name = format!("releases.{}.json", channel); let releases_name = format!("releases.{}.json", channel);
let path = self.url.trim_end_matches('/').to_owned() + "/"; let path = self.url.trim_end_matches('/').to_owned() + "/";
@@ -129,7 +129,7 @@ impl FileSource {
} }
impl UpdateSource for FileSource { impl UpdateSource for FileSource {
fn get_release_feed(&self, channel: &str, _: &manifest::Manifest) -> Result<VelopackAssetFeed, Error> { fn get_release_feed(&self, channel: &str, _: &bundle::Manifest) -> Result<VelopackAssetFeed, Error> {
let releases_name = format!("releases.{}.json", channel); let releases_name = format!("releases.{}.json", channel);
let releases_path = self.path.join(&releases_name); let releases_path = self.path.join(&releases_name);