use crate::{
bundle::{self, Manifest},
lockfile::LockFile,
misc, Error,
};
use semver::Version;
use std::path::{Path, PathBuf};
use uuid::Uuid;
#[cfg(windows)]
use crate::known_path::get_local_app_data;
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ShortcutLocationFlags: u32 {
const NONE = 0;
const START_MENU = 1 << 0;
const DESKTOP = 1 << 1;
const STARTUP = 1 << 2;
const START_MENU_ROOT = 1 << 4;
const USER_PINNED = 1 << 5;
}
}
impl ShortcutLocationFlags {
pub fn from_string(input: &str) -> ShortcutLocationFlags {
let mut flags = ShortcutLocationFlags::NONE;
for part in input.split([',', ';']) {
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
}
}
#[allow(non_snake_case)]
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Default)]
pub struct VelopackLocatorConfig {
pub RootAppDir: PathBuf,
pub UpdateExePath: PathBuf,
pub PackagesDir: PathBuf,
pub ManifestPath: PathBuf,
pub CurrentBinaryDir: PathBuf,
pub IsPortable: bool,
}
impl VelopackLocatorConfig {
pub fn load_manifest(&self) -> Result<Manifest, Error> {
read_current_manifest(&self.ManifestPath)
}
}
#[derive(Clone)]
pub struct VelopackLocator {
paths: VelopackLocatorConfig,
manifest: Manifest,
}
impl TryFrom<VelopackLocatorConfig> for VelopackLocator {
type Error = Error;
fn try_from(config: VelopackLocatorConfig) -> Result<Self, Self::Error> {
VelopackLocator::new(&config)
}
}
impl TryFrom<&VelopackLocatorConfig> for VelopackLocator {
type Error = Error;
fn try_from(config: &VelopackLocatorConfig) -> Result<Self, Self::Error> {
VelopackLocator::new(config)
}
}
impl TryFrom<LocationContext> for VelopackLocator {
type Error = Error;
fn try_from(context: LocationContext) -> Result<Self, Self::Error> {
auto_locate_app_manifest(context)
}
}
impl TryFrom<&LocationContext> for VelopackLocator {
type Error = Error;
fn try_from(context: &LocationContext) -> Result<Self, Self::Error> {
auto_locate_app_manifest(context.clone())
}
}
impl VelopackLocator {
pub fn new(config: &VelopackLocatorConfig) -> Result<VelopackLocator, Error> {
if !config.UpdateExePath.exists() {
return Err(Error::NotInstalled(format!(
"Update.exe does not exist in the expected path ({})",
config.UpdateExePath.display()
)));
}
if !config.ManifestPath.exists() {
return Err(Error::NotInstalled(format!(
"Manifest file does not exist in the expected path ({})",
config.ManifestPath.display()
)));
}
let manifest = read_current_manifest(&config.ManifestPath)?;
Ok(Self::new_with_manifest(config.clone(), manifest))
}
#[cfg(windows)]
pub fn new_with_manifest(mut paths: VelopackLocatorConfig, manifest: Manifest) -> Self {
let root = paths.RootAppDir.clone();
let default_packages_dir = root.join("packages");
let has_custom_packages_dir = paths.PackagesDir != default_packages_dir;
if has_custom_packages_dir {
if misc::is_directory_writable(&paths.PackagesDir) {
info!("Using custom packages directory (writable): {}", paths.PackagesDir.display());
} else {
warn!(
"Custom packages directory is not writable, falling through to standard logic: {}",
paths.PackagesDir.display()
);
paths.PackagesDir = default_packages_dir.clone();
}
}
if paths.PackagesDir == default_packages_dir {
let is_writable = misc::is_directory_writable(&root);
info!("Root directory '{}' writable: {}", root.display(), is_writable);
if is_writable {
paths.PackagesDir = root.join("packages");
info!("Using root packages directory: {}", paths.PackagesDir.display());
} else if let Ok(app_data) = get_local_app_data() {
let fallback_base = app_data.join(&manifest.id);
paths.PackagesDir = fallback_base.join("packages");
paths.UpdateExePath = fallback_base.join("Update.exe");
info!("Using fallback directory: {}", fallback_base.display());
if let Err(e) = std::fs::create_dir_all(&paths.PackagesDir) {
error!("Unable to create fallback packages directory: {}", e);
}
let root_update_exe = root.join("Update.exe");
if !paths.UpdateExePath.exists() && root_update_exe.exists() {
match std::fs::copy(&root_update_exe, &paths.UpdateExePath) {
Ok(_) => info!("Copied Update.exe from root to fallback: {}", paths.UpdateExePath.display()),
Err(e) => error!("Failed to copy Update.exe to fallback path: {}", e),
}
}
} else {
error!("Root directory is not writable and LocalAppData is unavailable. Updates may not work correctly.");
}
}
Self { paths, manifest }
}
#[cfg(not(windows))]
pub fn new_with_manifest(paths: VelopackLocatorConfig, manifest: Manifest) -> Self {
Self { paths, manifest }
}
pub fn get_packages_dir(&self) -> PathBuf {
self.paths.PackagesDir.clone()
}
pub fn get_ideal_local_nupkg_path(&self, id: Option<&str>, version: Option<Version>) -> PathBuf {
let id = id.unwrap_or(&self.manifest.id);
let version = version.unwrap_or(self.manifest.version.clone());
self.paths.PackagesDir.join(format!("{}-{}-full.nupkg", id, version))
}
pub fn get_temp_dir_root(&self) -> PathBuf {
self.paths.PackagesDir.join("VelopackTemp")
}
pub fn get_temp_dir_rand16(&self) -> PathBuf {
self.get_temp_dir_root().join("tmp_".to_string() + &misc::random_string(16))
}
#[cfg(not(target_os = "linux"))]
pub fn get_root_dir(&self) -> PathBuf {
self.paths.RootAppDir.clone()
}
#[cfg(target_os = "linux")]
pub fn get_appimage_path(&self) -> PathBuf {
self.paths.RootAppDir.clone()
}
pub fn get_update_path(&self) -> PathBuf {
self.paths.UpdateExePath.clone()
}
pub fn get_main_exe_path(&self) -> PathBuf {
self.paths.CurrentBinaryDir.join(&self.manifest.main_exe)
}
pub fn get_current_bin_dir(&self) -> PathBuf {
self.paths.CurrentBinaryDir.clone()
}
pub fn get_manifest(&self) -> Manifest {
self.manifest.clone()
}
pub fn get_manifest_version(&self) -> Version {
self.manifest.version.clone()
}
pub fn get_staged_user_id(&self) -> String {
self.get_or_create_staged_user_id().clone()
}
pub fn get_manifest_version_full_string(&self) -> String {
self.manifest.version.to_string()
}
pub fn get_manifest_version_short_string(&self) -> String {
let ver = &self.manifest.version;
format!("{}.{}.{}", ver.major, ver.minor, ver.patch)
}
pub fn get_manifest_channel(&self) -> String {
self.manifest.channel.clone()
}
pub fn get_manifest_id(&self) -> String {
self.manifest.id.clone()
}
pub fn get_manifest_title(&self) -> String {
self.manifest.title.clone()
}
pub fn get_manifest_authors(&self) -> String {
self.manifest.authors.clone()
}
pub fn get_manifest_shortcut_locations(&self) -> ShortcutLocationFlags {
if self.manifest.shortcut_locations.is_empty() {
return ShortcutLocationFlags::NONE;
}
if self.manifest.shortcut_locations.eq_ignore_ascii_case("none") {
return ShortcutLocationFlags::NONE;
}
ShortcutLocationFlags::from_string(&self.manifest.shortcut_locations)
}
pub fn get_manifest_shortcut_aumid(&self) -> Option<String> {
if self.manifest.shortcut_aumid.is_empty() {
return None;
}
Some(self.manifest.shortcut_aumid.clone())
}
pub fn get_app_user_model_id(&self) -> String {
self.get_manifest_shortcut_aumid()
.unwrap_or_else(|| format!("velopack.{}", self.manifest.id))
}
pub fn clone_self_with_new_manifest(&self, manifest: &Manifest) -> VelopackLocator {
VelopackLocator {
paths: self.paths.clone(),
manifest: manifest.clone(),
}
}
pub fn get_is_portable(&self) -> bool {
self.paths.IsPortable
}
#[cfg(windows)]
pub fn get_is_msi_install(&self) -> bool {
self.paths.RootAppDir.join(".msi-installed").exists()
}
pub fn try_get_exclusive_lock(&self) -> Result<LockFile, Error> {
info!("Attempting to acquire exclusive lock on packages directory (non-blocking)...");
let packages_dir = self.get_packages_dir();
std::fs::create_dir_all(&packages_dir)?;
let lock_file_path = packages_dir.join(".velopack_lock");
let lock_file = LockFile::try_acquire_lock(&lock_file_path)?;
Ok(lock_file)
}
fn get_or_create_staged_user_id(&self) -> String {
let packages_dir = self.get_packages_dir();
let beta_id_path = packages_dir.join(".betaId");
if beta_id_path.exists() {
info!("Found existing staged user id...");
if let Ok(beta_id) = std::fs::read_to_string(&beta_id_path) {
return beta_id;
}
}
let new_id = Uuid::new_v4();
if let Err(_e) = std::fs::write(&beta_id_path, new_id.to_string()) {
warn!("Couldn't write out staging userId.");
} else {
info!("Generated new staging userId: {}", new_id);
}
new_id.to_string()
}
}
#[cfg(target_os = "windows")]
pub fn create_config_from_root_dir<P: AsRef<std::path::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(),
}
}
#[derive(Debug, Clone)]
pub enum LocationContext {
Unknown,
IAmUpdateExe,
FromCurrentExe,
FromSpecifiedRootDir(PathBuf, Option<PathBuf>),
FromSpecifiedAppExecutable(PathBuf),
}
#[cfg(target_os = "windows")]
pub fn auto_locate_app_manifest(context: LocationContext) -> Result<VelopackLocator, Error> {
info!("Auto-locating app manifest...");
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, package_dir) => {
let mut config = create_config_from_root_dir(&root_dir);
if let Some(pkg_dir) = package_dir {
config.PackagesDir = pkg_dir;
}
let locator = VelopackLocator::new(&config)?;
return Ok(locator);
}
LocationContext::FromSpecifiedAppExecutable(exe_path) => {
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 = VelopackLocator::new(&config)?;
return Ok(locator);
}
}
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 = VelopackLocator::new(&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 = VelopackLocator::new(&config)?;
return Ok(locator);
}
}
};
Err(Error::NotInstalled("Could not auto-locate app manifest".to_owned()))
}
#[cfg(target_os = "linux")]
pub fn auto_locate_app_manifest(context: LocationContext) -> Result<VelopackLocator, Error> {
let mut search_path = std::env::current_exe()?;
let mut package_dir_override: Option<PathBuf> = None;
let mut appimage_path_override: Option<PathBuf> = None;
match context {
LocationContext::FromSpecifiedRootDir(dir, pkg_dir) => {
if dir.is_file() {
appimage_path_override = Some(dir);
}
package_dir_override = pkg_dir;
}
LocationContext::FromSpecifiedAppExecutable(exe) => search_path = exe,
_ => {}
}
let search_string = search_path.to_string_lossy();
let idx = search_string.find("/usr/bin/");
if idx.is_none() {
return Err(Error::NotInstalled(format!(
"Could not locate '/usr/bin/' in executable path {}",
search_string
)));
}
let idx = idx.unwrap();
let mount_dir = PathBuf::from(search_string[..idx].to_string());
let contents_dir = mount_dir.join("usr").join("bin");
let update_exe_path = contents_dir.join("UpdateNix");
let metadata_path = contents_dir.join("sq.version");
if !update_exe_path.exists() {
return Err(Error::NotInstalled(format!(
"UpdateNix does not exist at the expected path: {}",
update_exe_path.to_string_lossy()
)));
}
let appimage_from_env = std::env::var("APPIMAGE")
.ok()
.filter(|v| !v.is_empty() && PathBuf::from(v).exists())
.map(PathBuf::from);
let appimage_path = if let Some(p) = appimage_path_override {
if p.exists() {
p
} else if let Some(fallback) = appimage_from_env.clone() {
error!(
"Specified AppImage path '{}' does not exist, falling back to $APPIMAGE='{}'",
p.to_string_lossy(),
fallback.to_string_lossy()
);
fallback
} else {
return Err(Error::NotInstalled(format!(
"The specified AppImage path does not exist: {}",
p.to_string_lossy()
)));
}
} else if let Some(p) = appimage_from_env {
p
} else {
let env_val = std::env::var("APPIMAGE").unwrap_or_default();
return Err(Error::NotInstalled(if env_val.is_empty() {
"The $APPIMAGE environment variable is not set. Is this app running as an AppImage?".to_string()
} else {
format!("The $APPIMAGE environment variable points to a path that does not exist: {}", env_val)
}));
};
info!("Resolved AppImage path: {}", appimage_path.to_string_lossy());
let app = read_current_manifest(&metadata_path)?;
let packages_dir = if let Some(pkg_dir) = package_dir_override {
pkg_dir
} else {
PathBuf::from("/var/tmp/velopack").join(&app.id).join("packages")
};
let config = VelopackLocatorConfig {
RootAppDir: appimage_path,
UpdateExePath: update_exe_path,
PackagesDir: packages_dir,
ManifestPath: metadata_path,
CurrentBinaryDir: contents_dir,
IsPortable: true,
};
Ok(VelopackLocator::new_with_manifest(config, app))
}
#[cfg(target_os = "macos")]
pub fn auto_locate_app_manifest(context: LocationContext) -> Result<VelopackLocator, Error> {
let mut search_path = std::env::current_exe()?;
let mut package_dir_override: Option<PathBuf> = None;
match context {
LocationContext::FromSpecifiedRootDir(dir, pkg_dir) => {
search_path = dir.join("dummy");
package_dir_override = pkg_dir;
}
LocationContext::FromSpecifiedAppExecutable(exe) => search_path = exe,
_ => {}
}
let search_string = search_path.to_string_lossy();
let idx = search_string.find(".app/");
if idx.is_none() {
return Err(Error::NotInstalled(format!(
"Could not locate '.app' in executable path {}",
search_string
)));
}
let idx = idx.unwrap();
let path = search_string[..(idx + 4)].to_string();
let root_app_dir = PathBuf::from(&path);
let contents_dir = root_app_dir.join("Contents").join("MacOS");
let update_exe_path = contents_dir.join("UpdateMac");
let metadata_path = contents_dir.join("sq.version");
let resources_metadata_path = root_app_dir.join("Contents").join("Resources").join("sq.version");
if !update_exe_path.exists() {
return Err(Error::NotInstalled("UpdateMac does not exist in the expected path".to_owned()));
}
let (app, resolved_metadata_path) = read_current_manifest(&metadata_path)
.map(|m| (m, metadata_path))
.or_else(|_| read_current_manifest(&resources_metadata_path).map(|m| (m, resources_metadata_path)))?;
let packages_dir = if let Some(pkg_dir) = package_dir_override {
pkg_dir
} else {
#[allow(deprecated)]
let mut dir = std::env::home_dir().expect("Could not locate user home directory via $HOME or /etc/passwd");
dir.push("Library");
dir.push("Caches");
dir.push("velopack");
dir.push(&app.id);
dir.push("packages");
dir
};
let config = VelopackLocatorConfig {
RootAppDir: root_app_dir,
UpdateExePath: update_exe_path,
PackagesDir: packages_dir,
ManifestPath: resolved_metadata_path,
CurrentBinaryDir: contents_dir,
IsPortable: true,
};
Ok(VelopackLocator::new_with_manifest(config, app))
}
fn read_current_manifest(nuspec_path: &Path) -> Result<Manifest, Error> {
if nuspec_path.exists() {
if let Ok(nuspec) = misc::retry_io(|| std::fs::read_to_string(nuspec_path)) {
return bundle::read_manifest_from_string(&nuspec);
}
}
Err(Error::NotInstalled(format!(
"Manifest file does not exist or is not readable: {:?}",
nuspec_path
)))
}
pub fn find_local_full_packages(packages_dir: &Path) -> Vec<(PathBuf, Manifest)> {
let packages_dir_str = packages_dir.to_string_lossy();
info!("Searching for local packages in: {:?}", packages_dir_str);
let mut results = Vec::new();
let search_glob = format!("{}/*-full.nupkg", packages_dir_str);
if let Ok(paths) = glob::glob(search_glob.as_str()) {
for path in paths.into_iter().flatten() {
trace!("Checking package: '{:?}'", path);
if let Ok(mut bun) = bundle::load_bundle_from_file(&path) {
if let Ok(mani) = bun.read_manifest() {
info!("Found {}: '{:?}'", mani.version, path);
results.push((path, mani));
}
}
}
}
results
}
pub fn find_latest_full_package(packages_dir: &Path) -> Option<(PathBuf, Manifest)> {
find_local_full_packages(packages_dir)
.into_iter()
.max_by(|(_, a), (_, b)| a.version.cmp(&b.version))
}
#[test]
fn test_locator_staged_id_for_new_user() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp_buf = tmp_dir.path().to_path_buf();
let test_dir = tmp_buf.join(format!("velopack_{}", misc::random_string(8)));
let mut paths = VelopackLocatorConfig::default();
paths.PackagesDir = test_dir;
assert!(std::fs::create_dir_all(&paths.PackagesDir).is_ok());
let locator = VelopackLocator::new_with_manifest(paths, Manifest::default());
let staged_user_id = locator.get_staged_user_id();
assert_ne!(staged_user_id, "");
let packages_dir = locator.get_packages_dir();
let beta_id_path = packages_dir.join(".betaId");
assert!(beta_id_path.exists());
if let Ok(beta_id) = std::fs::read_to_string(&beta_id_path) {
assert_eq!(staged_user_id, beta_id);
} else {
assert!(false, "Couldn't read staging userId.");
}
}
#[test]
fn test_locator_staged_id_for_existing_user() {
let tmp_dir = tempfile::TempDir::new().unwrap();
let tmp_buf = tmp_dir.path().to_path_buf();
let test_dir = tmp_buf.join(format!("velopack_{}", misc::random_string(8)));
let mut paths = VelopackLocatorConfig::default();
paths.PackagesDir = test_dir;
assert!(std::fs::create_dir_all(&paths.PackagesDir).is_ok());
let locator = VelopackLocator::new_with_manifest(paths, Manifest::default());
let packages_dir = locator.get_packages_dir();
let beta_id_path = packages_dir.join(".betaId");
let expected_user_id = "test user id";
std::fs::write(&beta_id_path, expected_user_id).unwrap();
let staged_user_id = locator.get_staged_user_id();
assert_eq!(expected_user_id, staged_user_id);
}