use std::path::PathBuf;
use semver::Version;
use uuid::Uuid;
use crate::{
bundle::{self, Manifest},
util, Error,
lockfile::LockFile
};
pub fn default_channel_name() -> String {
#[cfg(target_os = "windows")]
return "win".to_owned();
#[cfg(target_os = "linux")]
return "linux".to_owned();
#[cfg(target_os = "macos")]
return "osx".to_owned();
}
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(|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
}
}
#[allow(non_snake_case)]
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Default)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
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 VelopackLocator {
pub fn new(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(Self { paths: config.clone(), manifest })
}
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_packages_dir_as_string(&self) -> String {
Self::path_as_string(&self.paths.PackagesDir)
}
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.RootAppDir.join("packages").join(format!("{}-{}-full.nupkg", id, version))
}
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))
}
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() + &util::random_string(16))
}
pub fn get_temp_dir_as_string(&self) -> String {
Self::path_as_string(&self.get_temp_dir_root())
}
pub fn get_root_dir(&self) -> PathBuf {
self.paths.RootAppDir.clone()
}
pub fn get_root_dir_as_string(&self) -> String {
Self::path_as_string(&self.paths.RootAppDir)
}
pub fn get_update_path(&self) -> PathBuf {
self.paths.UpdateExePath.clone()
}
pub fn get_update_path_as_string(&self) -> String {
Self::path_as_string(&self.paths.UpdateExePath)
}
pub fn get_main_exe_path(&self) -> PathBuf {
self.paths.CurrentBinaryDir.join(&self.manifest.main_exe)
}
pub fn get_main_exe_path_as_string(&self) -> String {
Self::path_as_string(&self.get_main_exe_path())
}
pub fn get_current_bin_dir(&self) -> PathBuf {
self.paths.CurrentBinaryDir.clone()
}
pub fn get_current_bin_dir_as_string(&self) -> String {
Self::path_as_string(&self.paths.CurrentBinaryDir)
}
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.to_ascii_lowercase() == "none" {
return ShortcutLocationFlags::NONE;
}
ShortcutLocationFlags::from_string(&self.manifest.shortcut_locations)
}
pub fn get_manifest_shortcut_amuid(&self) -> Option<String> {
if self.manifest.shortcut_amuid.is_empty() {
return None;
}
Some(self.manifest.shortcut_amuid.clone())
}
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
}
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 path_as_string(path: &PathBuf) -> String {
path.to_string_lossy().to_string()
}
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.to_string());
}
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(),
}
}
pub enum LocationContext
{
Unknown,
IAmUpdateExe,
FromCurrentExe,
FromSpecifiedRootDir(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) => {
let config = create_config_from_root_dir(&root_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()?;
match context {
LocationContext::FromSpecifiedRootDir(dir) => search_path = dir.join("dummy"),
LocationContext::FromSpecifiedAppExecutable(exe) => search_path = exe,
_ => {}
}
let search_string = search_path.to_string_lossy();
let idx = search_string.rfind("/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 root_app_dir = PathBuf::from(search_string[..idx].to_string());
let contents_dir = root_app_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::MissingUpdateExe);
}
let appimage_path = match std::env::var("APPIMAGE") {
Ok(v) => {
if v.is_empty() || !PathBuf::from(&v).exists() {
return Err(Error::NotInstalled("The 'APPIMAGE' environment variable should point to the current AppImage path.".to_string()));
} else {
v
}
},
Err(_) => {
return Err(Error::NotInstalled("The 'APPIMAGE' environment variable should point to the current AppImage path.".to_string()));
}
};
let app = read_current_manifest(&metadata_path)?;
let packages_dir = PathBuf::from("/var/tmp/velopack").join(&app.id).join("packages");
let config = VelopackLocatorConfig {
RootAppDir: PathBuf::from(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()?;
match context {
LocationContext::FromSpecifiedRootDir(dir) => search_path = dir.join("dummy"),
LocationContext::FromSpecifiedAppExecutable(exe) => search_path = exe,
_ => {}
}
let search_string = search_path.to_string_lossy();
let idx = search_string.rfind(".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");
if !update_exe_path.exists() {
return Err(Error::MissingUpdateExe);
}
let app = read_current_manifest(&metadata_path)?;
#[allow(deprecated)]
let mut packages_dir = std::env::home_dir().expect("Could not locate user home directory via $HOME or /etc/passwd");
packages_dir.push("Library");
packages_dir.push("Caches");
packages_dir.push("velopack");
packages_dir.push(&app.id);
packages_dir.push("packages");
let config = VelopackLocatorConfig {
RootAppDir: root_app_dir,
UpdateExePath: update_exe_path,
PackagesDir: packages_dir,
ManifestPath: metadata_path,
CurrentBinaryDir: contents_dir,
IsPortable: true,
};
Ok(VelopackLocator::new_with_manifest(config, app))
}
fn read_current_manifest(nuspec_path: &PathBuf) -> Result<Manifest, Error> {
if nuspec_path.exists() {
if let Ok(nuspec) = util::retry_io(|| std::fs::read_to_string(nuspec_path)) {
return bundle::read_manifest_from_string(&nuspec);
}
}
Err(Error::MissingNuspec)
}
pub fn find_latest_full_package(packages_dir: &PathBuf) -> Option<(PathBuf, Manifest)> {
let packages_dir = packages_dir.to_string_lossy();
info!("Attempting to auto-detect package in: {}", packages_dir);
let mut package: Option<(PathBuf, Manifest)> = None;
let search_glob = format!("{}/*-full.nupkg", packages_dir);
if let Ok(paths) = glob::glob(search_glob.as_str()) {
for path in paths.into_iter().flatten() {
trace!("Checking package: '{}'", path.to_string_lossy());
if let Ok(mut bun) = bundle::load_bundle_from_file(&path) {
if let Ok(mani) = bun.read_manifest() {
if package.is_none() || mani.version > package.clone()?.1.version {
info!("Found {}: '{}'", mani.version, path.to_string_lossy());
package = Some((path, mani));
}
}
}
}
}
package
}
#[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_{}", util::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_{}", util::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);
}