Setup support online installer modes

This commit is contained in:
Caelan Sayler
2024-12-03 06:03:44 +00:00
parent 51b039b43d
commit 530ae35592
3 changed files with 260 additions and 89 deletions

View File

@@ -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(())
}

View File

@@ -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(())
}
}
}

View File

@@ -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);