mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Setup support online installer modes
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
#![allow(dead_code)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
@@ -8,47 +7,125 @@ use anyhow::{bail, Result};
|
||||
use clap::{arg, value_parser, Command};
|
||||
use memmap2::Mmap;
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
use std::{env, path::PathBuf};
|
||||
use velopack::bundle::BundleZip;
|
||||
use velopack::sources::{HttpSource, UpdateSource};
|
||||
use velopack_bins::*;
|
||||
|
||||
#[repr(u8)]
|
||||
enum MultiPartMode {
|
||||
None = 0,
|
||||
Online = 1,
|
||||
LocalFilePart = 2,
|
||||
Embedded = 3,
|
||||
}
|
||||
|
||||
enum MultiPartResult {
|
||||
None,
|
||||
Online(bool, String, String, String),
|
||||
LocalFilePart(bool, String),
|
||||
AbsolutePath(bool, String),
|
||||
Embedded(bool, i64, i64),
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for MultiPartMode {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(value: u8) -> Result<Self> {
|
||||
match value {
|
||||
0 => Ok(MultiPartMode::None),
|
||||
1 => Ok(MultiPartMode::Online),
|
||||
2 => Ok(MultiPartMode::LocalFilePart),
|
||||
3 => Ok(MultiPartMode::Embedded),
|
||||
_ => bail!("Invalid value for MultiPartMode"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[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, //
|
||||
static MULTIPART_PLACEHOLDER: [u8; 2 + 1024 + 32] = [
|
||||
// 1 byte for multipart mode flag
|
||||
0,
|
||||
// requires elevated permissions
|
||||
0,
|
||||
// 1024 bytes for online multipart URL or local part path
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
// 32 bytes for multipart signature
|
||||
0x76, 0x70, 0x6b, 0x20, 0x6d, 0x75, 0x6c, 0x74,
|
||||
0x69, 0x70, 0x61, 0x72, 0x74, 0x2e, 0x2e, 0x2e,
|
||||
0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e,
|
||||
0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e, 0x2e,
|
||||
];
|
||||
|
||||
#[inline(never)]
|
||||
pub fn header_offset_and_length() -> (i64, i64) {
|
||||
fn multipart_header() -> Result<MultiPartResult> {
|
||||
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)
|
||||
// Perform a volatile read to avoid compiler optimization issues
|
||||
let mode: MultiPartMode = unsafe {
|
||||
let mode_number: u8 = ptr::read_volatile(&MULTIPART_PLACEHOLDER[0]);
|
||||
mode_number.try_into()?
|
||||
};
|
||||
|
||||
let elevated: bool = unsafe { ptr::read_volatile(&MULTIPART_PLACEHOLDER[1]) != 0 };
|
||||
|
||||
match mode {
|
||||
MultiPartMode::None => Ok(MultiPartResult::None),
|
||||
MultiPartMode::Online => {
|
||||
let url = String::from_utf8_lossy(&MULTIPART_PLACEHOLDER[1..1025]).trim_end_matches(char::from(0)).to_string();
|
||||
let parts: Vec<&str> = url.splitn(3, '\0').collect();
|
||||
let channel = parts.get(0).unwrap_or(&"").to_string();
|
||||
let filename = parts.get(1).unwrap_or(&"").to_string();
|
||||
let url = parts.get(2).unwrap_or(&"").to_string();
|
||||
if url.is_empty() || channel.is_empty() {
|
||||
bail!("Invalid online multipart URL or channel (missing)");
|
||||
}
|
||||
info!("MultiPart Online: Channel={}, Filename={}, URL={}", channel, filename, url);
|
||||
Ok(MultiPartResult::Online(elevated, channel, filename, url))
|
||||
}
|
||||
MultiPartMode::LocalFilePart => {
|
||||
let path = String::from_utf8_lossy(&MULTIPART_PLACEHOLDER[1..1025]).trim_end_matches(char::from(0)).to_string();
|
||||
info!("MultiPart LocalFilePart: Path={}", path);
|
||||
Ok(MultiPartResult::LocalFilePart(elevated, path))
|
||||
}
|
||||
MultiPartMode::Embedded => {
|
||||
let offset = i64::from_le_bytes(MULTIPART_PLACEHOLDER[1..9].try_into()?);
|
||||
let length = i64::from_le_bytes(MULTIPART_PLACEHOLDER[9..17].try_into()?);
|
||||
info!("MultiPart Embedded: Offset={}, Length={}", offset, length);
|
||||
Ok(MultiPartResult::Embedded(elevated, offset, length))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,12 +141,14 @@ fn main_inner() -> Result<()> {
|
||||
.arg(arg!(-s --silent "Hides all dialogs and answers 'yes' to all prompts"))
|
||||
.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!(-t --installto <DIR> "Installation directory to install the application").required(false).value_parser(value_parser!(PathBuf)))
|
||||
.arg(arg!(-t --install-to <DIR> "Installation directory to install the application").required(false).value_parser(value_parser!(PathBuf)))
|
||||
.arg(arg!(--no-first-run "Skip first application run after installation completed"))
|
||||
.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) {
|
||||
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!(--debug-file <FILE> "Debug mode, install from a nupkg file").required(false).value_parser(value_parser!(PathBuf)))
|
||||
.arg(arg!(--debug-online <URL> "Debug mode, install from an online feed").required(false));
|
||||
}
|
||||
|
||||
let matches = arg_config.try_get_matches()?;
|
||||
@@ -81,8 +160,10 @@ fn main_inner() -> Result<()> {
|
||||
let logfile = matches.get_one::<PathBuf>("log");
|
||||
logging::setup_logging("setup", logfile, true, verbose)?;
|
||||
|
||||
let debug = matches.get_one::<PathBuf>("debug");
|
||||
let install_to = matches.get_one::<PathBuf>("installto");
|
||||
let no_first_run = matches.get_flag("no-first-run");
|
||||
let debug_file = matches.get_one::<PathBuf>("debug-file");
|
||||
let debug_online = matches.get_one::<String>("debug-online");
|
||||
let install_to = matches.get_one::<PathBuf>("install-to");
|
||||
let exe_args: Option<Vec<&str>> = matches.get_many::<String>("EXE_ARGS").map(|v| v.map(|f| f.as_str()).collect());
|
||||
|
||||
info!("Starting Velopack Setup ({})", env!("NGBV_VERSION"));
|
||||
@@ -91,9 +172,6 @@ fn main_inner() -> Result<()> {
|
||||
info!(" Verbose: {}", verbose);
|
||||
info!(" Log: {:?}", logfile);
|
||||
info!(" Install To: {:?}", install_to);
|
||||
if cfg!(debug_assertions) {
|
||||
info!(" Debug: {:?}", debug);
|
||||
}
|
||||
|
||||
// change working directory to the containing directory of the exe
|
||||
let mut containing_dir = env::current_exe()?;
|
||||
@@ -109,30 +187,113 @@ fn main_inner() -> Result<()> {
|
||||
bail!("This installer requires Windows 7 SPA1 or later and cannot run.");
|
||||
}
|
||||
|
||||
// in debug mode only, allow a nupkg to be passed in as the first argument
|
||||
let mut installer_mode = multipart_header()?;
|
||||
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(());
|
||||
if let Some(pkg) = debug_file {
|
||||
warn!("Loading bundle from DEBUG nupkg file {:?}...", pkg);
|
||||
installer_mode = MultiPartResult::AbsolutePath(false, pkg.to_string_lossy().to_string());
|
||||
} else if let Some(url) = debug_online {
|
||||
warn!("Loading bundle from DEBUG online feed {:?}...", url);
|
||||
let parts: Vec<&str> = url.splitn(2, ';').collect();
|
||||
installer_mode =
|
||||
MultiPartResult::Online(false, parts.get(0).unwrap().to_string(), "".to_string(), parts.get(1).unwrap().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
info!("Reading bundle header...");
|
||||
let (offset, length) = header_offset_and_length();
|
||||
info!("Bundle offset = {}, length = {}", offset, length);
|
||||
let my_exe_path = env::current_exe()?;
|
||||
|
||||
// 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)?;
|
||||
match installer_mode {
|
||||
MultiPartResult::None => {
|
||||
bail!("No installer mode detected. Please contact the application author.");
|
||||
}
|
||||
MultiPartResult::Online(elevate, channel, filename, url) => {
|
||||
let package_path = if filename.is_empty() {
|
||||
let mut local_path = my_exe_path.clone();
|
||||
local_path.set_extension("supart");
|
||||
local_path
|
||||
} else {
|
||||
let mut local_path = my_exe_path.clone();
|
||||
local_path.pop();
|
||||
local_path.push(filename);
|
||||
local_path
|
||||
};
|
||||
|
||||
main_download_online_package(&package_path, channel, url)?;
|
||||
let mut bundle = velopack::bundle::load_bundle_from_file(&package_path)?;
|
||||
main_install(&mut bundle, elevate, no_first_run, install_to, exe_args)?;
|
||||
}
|
||||
MultiPartResult::AbsolutePath(elevate, path) => {
|
||||
let mut bundle = velopack::bundle::load_bundle_from_file(&path)?;
|
||||
main_install(&mut bundle, elevate, no_first_run, install_to, exe_args)?;
|
||||
}
|
||||
MultiPartResult::LocalFilePart(elevate, filename) => {
|
||||
let package_path = if filename.is_empty() {
|
||||
let mut local_path = my_exe_path.clone();
|
||||
local_path.set_extension("supart");
|
||||
local_path
|
||||
} else {
|
||||
let mut local_path = my_exe_path.clone();
|
||||
local_path.pop();
|
||||
local_path.push(filename);
|
||||
local_path
|
||||
};
|
||||
let mut bundle = velopack::bundle::load_bundle_from_file(&package_path)?;
|
||||
main_install(&mut bundle, elevate, no_first_run, install_to, exe_args)?;
|
||||
}
|
||||
MultiPartResult::Embedded(elevate, offset, length) => {
|
||||
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)?;
|
||||
main_install(&mut bundle, elevate, no_first_run, install_to, exe_args)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main_download_online_package<T: AsRef<Path>>(local_path: T, channel: String, url: String) -> Result<()> {
|
||||
let local_path = local_path.as_ref();
|
||||
if local_path.exists() {
|
||||
info!("Local file already exists, skipping download.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
bail!("Could not find embedded zip file. Please contact the application author.");
|
||||
let source = HttpSource::new(&url);
|
||||
let feed = source.get_release_feed(&channel, None, None)?;
|
||||
|
||||
let asset = feed.get_latest();
|
||||
if asset.is_none() {
|
||||
bail!("No assets found in feed remote.");
|
||||
}
|
||||
|
||||
let asset = asset.unwrap();
|
||||
|
||||
let tx = if dialogs::get_silent() {
|
||||
info!("Will not show progress because silent mode is on.");
|
||||
let (tx, _) = std::sync::mpsc::channel::<i16>();
|
||||
tx
|
||||
} else {
|
||||
info!("Showing progress dialog...");
|
||||
let exe_path = env::current_exe()?;
|
||||
let exe_name = exe_path.file_name().unwrap().to_string_lossy();
|
||||
windows::splash::show_progress_dialog(exe_name, "Please wait while the installer is downloading the package...")
|
||||
};
|
||||
|
||||
source.download_release_entry(&asset, local_path.to_str().unwrap(), Some(tx.clone()))?;
|
||||
let _ = tx.send(windows::splash::MSG_CLOSE);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main_install(
|
||||
bundle: &mut BundleZip,
|
||||
_required_elevated: bool,
|
||||
_no_first_run: bool,
|
||||
install_to: Option<&PathBuf>,
|
||||
start_args: Option<Vec<&str>>,
|
||||
) -> Result<()> {
|
||||
// TODO: implement elevation here if required
|
||||
commands::install(bundle, install_to, start_args)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -14,10 +14,9 @@ use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
locator::{self, VelopackLocatorConfig, LocationContext, VelopackLocator},
|
||||
locator::{self, LocationContext, VelopackLocator, VelopackLocatorConfig},
|
||||
sources::UpdateSource,
|
||||
Error,
|
||||
util,
|
||||
util, Error,
|
||||
};
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
@@ -34,6 +33,11 @@ impl VelopackAssetFeed {
|
||||
pub fn find(&self, release_name: &str) -> Option<&VelopackAsset> {
|
||||
self.Assets.iter().find(|x| x.FileName.eq_ignore_ascii_case(release_name))
|
||||
}
|
||||
|
||||
/// Get the latest release in the feed, or None if the feed is empty.
|
||||
pub fn get_latest(&self) -> Option<&VelopackAsset> {
|
||||
self.Assets.iter().max_by(|a, b| a.Version.cmp(&b.Version))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
@@ -189,7 +193,7 @@ impl UpdateManager {
|
||||
pub fn get_current_version_as_string(&self) -> String {
|
||||
self.locator.get_manifest_version_full_string()
|
||||
}
|
||||
|
||||
|
||||
/// The currently installed app version as a semver Version.
|
||||
pub fn get_current_version(&self) -> Version {
|
||||
self.locator.get_manifest_version()
|
||||
@@ -206,7 +210,7 @@ impl UpdateManager {
|
||||
self.locator.get_is_portable()
|
||||
}
|
||||
|
||||
/// Returns None if there is no local package waiting to be applied. Returns a VelopackAsset
|
||||
/// Returns None if there is no local package waiting to be applied. Returns a VelopackAsset
|
||||
/// if there is an update downloaded which has not yet been applied. In that case, the
|
||||
/// VelopackAsset can be applied by calling apply_updates_and_restart or wait_exit_then_apply_updates.
|
||||
pub fn get_update_pending_restart(&self) -> Option<VelopackAsset> {
|
||||
@@ -232,13 +236,13 @@ impl UpdateManager {
|
||||
/// Get a list of available remote releases from the package source.
|
||||
pub fn get_release_feed(&self) -> Result<VelopackAssetFeed, Error> {
|
||||
let channel = self.get_practical_channel();
|
||||
self.source.get_release_feed(&channel, &self.locator.get_manifest())
|
||||
let manifest = self.locator.get_manifest();
|
||||
self.source.get_release_feed(&channel, Some(&manifest.id), Some(&manifest.version.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
/// Get a list of available remote releases from the package source.
|
||||
pub fn get_release_feed_async(&self) -> JoinHandle<Result<VelopackAssetFeed, Error>>
|
||||
{
|
||||
pub fn get_release_feed_async(&self) -> JoinHandle<Result<VelopackAssetFeed, Error>> {
|
||||
let self_clone = self.clone();
|
||||
async_std::task::spawn_blocking(move || self_clone.get_release_feed())
|
||||
}
|
||||
@@ -302,8 +306,7 @@ impl UpdateManager {
|
||||
#[cfg(feature = "async")]
|
||||
/// 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.
|
||||
pub fn check_for_updates_async(&self) -> JoinHandle<Result<UpdateCheck, Error>>
|
||||
{
|
||||
pub fn check_for_updates_async(&self) -> JoinHandle<Result<UpdateCheck, Error>> {
|
||||
let self_clone = self.clone();
|
||||
async_std::task::spawn_blocking(move || self_clone.check_for_updates())
|
||||
}
|
||||
@@ -331,7 +334,7 @@ impl UpdateManager {
|
||||
let old_nupkg_pattern = format!("{}/*.nupkg", packages_dir.to_string_lossy());
|
||||
let old_partial_pattern = format!("{}/*.partial", packages_dir.to_string_lossy());
|
||||
let mut to_delete = Vec::new();
|
||||
|
||||
|
||||
fn find_files_to_delete(pattern: &str, to_delete: &mut Vec<String>) {
|
||||
info!("Searching for packages to clean: '{}'", pattern);
|
||||
match glob::glob(pattern) {
|
||||
@@ -345,13 +348,13 @@ impl UpdateManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
find_files_to_delete(&old_nupkg_pattern, &mut to_delete);
|
||||
find_files_to_delete(&old_partial_pattern, &mut to_delete);
|
||||
|
||||
self.source.download_release_entry(&update.TargetFullRelease, &partial_file.to_string_lossy(), progress)?;
|
||||
info!("Successfully placed file: '{}'", partial_file.to_string_lossy());
|
||||
|
||||
|
||||
info!("Renaming partial file to final target: '{}'", final_target_file.to_string_lossy());
|
||||
fs::rename(&partial_file, &final_target_file)?;
|
||||
|
||||
@@ -424,7 +427,7 @@ impl UpdateManager {
|
||||
where
|
||||
A: AsRef<VelopackAsset>,
|
||||
S: AsRef<str>,
|
||||
C: IntoIterator<Item=S>,
|
||||
C: IntoIterator<Item = S>,
|
||||
{
|
||||
self.wait_exit_then_apply_updates(to_apply, false, true, restart_args)?;
|
||||
exit(0);
|
||||
@@ -449,7 +452,7 @@ impl UpdateManager {
|
||||
where
|
||||
A: AsRef<VelopackAsset>,
|
||||
S: AsRef<str>,
|
||||
C: IntoIterator<Item=S>,
|
||||
C: IntoIterator<Item = S>,
|
||||
{
|
||||
let to_apply = to_apply.as_ref();
|
||||
let pkg_path = self.locator.get_packages_dir().join(&to_apply.FileName);
|
||||
@@ -495,4 +498,4 @@ impl UpdateManager {
|
||||
p.spawn()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::*;
|
||||
use crate::bundle::Manifest;
|
||||
|
||||
/// Abstraction for finding and downloading updates from a package source / repository.
|
||||
/// An implementation may copy a file from a local repository, download from a web address,
|
||||
@@ -12,7 +11,7 @@ use crate::bundle::Manifest;
|
||||
pub trait UpdateSource: Send + Sync {
|
||||
/// Retrieve the list of available remote releases from the package source. These releases
|
||||
/// can subsequently be downloaded with download_release_entry.
|
||||
fn get_release_feed(&self, channel: &str, app: &bundle::Manifest) -> Result<VelopackAssetFeed, Error>;
|
||||
fn get_release_feed(&self, channel: &str, app_id: Option<&str>, local_version: Option<&str>) -> Result<VelopackAssetFeed, Error>;
|
||||
/// 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>;
|
||||
/// Clone the source to create a new lifetime.
|
||||
@@ -25,15 +24,20 @@ impl Clone for Box<dyn UpdateSource> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A source that does not provide any update capability.
|
||||
/// A source that does not provide any update capability.
|
||||
#[derive(Clone)]
|
||||
pub struct NoneSource {}
|
||||
|
||||
impl UpdateSource for NoneSource {
|
||||
fn get_release_feed(&self, _channel: &str, _app: &Manifest) -> Result<VelopackAssetFeed, Error> {
|
||||
fn get_release_feed(&self, _channel: &str, _app_id: Option<&str>, _local_version: Option<&str>) -> Result<VelopackAssetFeed, Error> {
|
||||
Err(Error::Generic("None source does not checking release feed".to_owned()))
|
||||
}
|
||||
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> {
|
||||
Err(Error::Generic("None source does not support downloads".to_owned()))
|
||||
}
|
||||
fn clone_boxed(&self) -> Box<dyn UpdateSource> {
|
||||
@@ -51,11 +55,8 @@ pub struct AutoSource {
|
||||
impl AutoSource {
|
||||
/// Create a new AutoSource with the specified input string.
|
||||
pub fn new(input: &str) -> AutoSource {
|
||||
let source: Box<dyn UpdateSource> = if Self::is_http_url(input) {
|
||||
Box::new(HttpSource::new(input))
|
||||
} else {
|
||||
Box::new(FileSource::new(input))
|
||||
};
|
||||
let source: Box<dyn UpdateSource> =
|
||||
if Self::is_http_url(input) { Box::new(HttpSource::new(input)) } else { Box::new(FileSource::new(input)) };
|
||||
AutoSource { source }
|
||||
}
|
||||
|
||||
@@ -68,8 +69,8 @@ impl AutoSource {
|
||||
}
|
||||
|
||||
impl UpdateSource for AutoSource {
|
||||
fn get_release_feed(&self, channel: &str, app: &bundle::Manifest) -> Result<VelopackAssetFeed, Error> {
|
||||
self.source.get_release_feed(channel, app)
|
||||
fn get_release_feed(&self, channel: &str, app_id: Option<&str>, local_version: Option<&str>) -> Result<VelopackAssetFeed, Error> {
|
||||
self.source.get_release_feed(channel, app_id, local_version)
|
||||
}
|
||||
|
||||
fn download_release_entry(&self, asset: &VelopackAsset, local_file: &str, progress_sender: Option<Sender<i16>>) -> Result<(), Error> {
|
||||
@@ -97,13 +98,19 @@ impl HttpSource {
|
||||
}
|
||||
|
||||
impl UpdateSource for HttpSource {
|
||||
fn get_release_feed(&self, channel: &str, app: &bundle::Manifest) -> Result<VelopackAssetFeed, Error> {
|
||||
fn get_release_feed(&self, channel: &str, app_id: Option<&str>, local_version: Option<&str>) -> Result<VelopackAssetFeed, Error> {
|
||||
let releases_name = format!("releases.{}.json", channel);
|
||||
|
||||
let path = self.url.trim_end_matches('/').to_owned() + "/";
|
||||
let url = url::Url::parse(&path)?;
|
||||
let mut releases_url = url.join(&releases_name)?;
|
||||
releases_url.set_query(Some(format!("localVersion={}&id={}", app.version, app.id).as_str()));
|
||||
|
||||
if let Some(app_id) = app_id {
|
||||
releases_url.query_pairs_mut().append_pair("id", app_id);
|
||||
}
|
||||
if let Some(local_version) = local_version {
|
||||
releases_url.query_pairs_mut().append_pair("localVersion", local_version);
|
||||
}
|
||||
|
||||
info!("Downloading releases for channel {} from: {}", channel, releases_url.to_string());
|
||||
let json = download::download_url_as_string(releases_url.as_str())?;
|
||||
@@ -146,7 +153,7 @@ impl FileSource {
|
||||
}
|
||||
|
||||
impl UpdateSource for FileSource {
|
||||
fn get_release_feed(&self, channel: &str, _: &bundle::Manifest) -> Result<VelopackAssetFeed, Error> {
|
||||
fn get_release_feed(&self, channel: &str, _app_id: Option<&str>, _local_version: Option<&str>) -> Result<VelopackAssetFeed, Error> {
|
||||
let releases_name = format!("releases.{}.json", channel);
|
||||
let releases_path = self.path.join(&releases_name);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user