use std::{path::PathBuf, process::Command};
use hdiff_update_core::{
apply_patch as core_apply_patch, create_patch as core_create_patch,
prepare_update as core_prepare_update, sha256_file, verify_sha256, ApplyPatchOptions,
CreatePatchOptions, DownloadEvent, FileDigest, PatchApplyResult, PatchCreateResult,
PrepareUpdateOptions, PreparedUpdate,
};
use serde::{Deserialize, Serialize};
use tauri::ipc::Channel;
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CommandError {
pub message: String,
}
impl<E> From<E> for CommandError
where
E: std::fmt::Display,
{
fn from(error: E) -> Self {
Self {
message: error.to_string(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RunInstallerOptions {
pub installer_path: PathBuf,
#[serde(default)]
pub args: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_dir: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expected_sha256: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DownloadAndInstallOptions {
pub update: PrepareUpdateOptions,
#[serde(default)]
pub installer_args: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_dir: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RunInstallerResult {
pub pid: u32,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DownloadAndInstallResult {
pub update: PreparedUpdate,
pub pid: u32,
}
#[tauri::command]
pub async fn artifact_sha256(path: PathBuf) -> Result<FileDigest, CommandError> {
tokio::task::spawn_blocking(move || sha256_file(path))
.await
.map_err(CommandError::from)?
.map_err(CommandError::from)
}
#[tauri::command]
pub async fn create_patch(options: CreatePatchOptions) -> Result<PatchCreateResult, CommandError> {
tokio::task::spawn_blocking(move || core_create_patch(&options))
.await
.map_err(CommandError::from)?
.map_err(CommandError::from)
}
#[tauri::command]
pub async fn apply_patch(options: ApplyPatchOptions) -> Result<PatchApplyResult, CommandError> {
tokio::task::spawn_blocking(move || core_apply_patch(&options))
.await
.map_err(CommandError::from)?
.map_err(CommandError::from)
}
#[tauri::command]
pub async fn download_and_apply(
options: PrepareUpdateOptions,
on_event: Channel<DownloadEvent>,
) -> Result<PreparedUpdate, CommandError> {
core_prepare_update(options, |event| {
let _ = on_event.send(event);
})
.await
.map_err(CommandError::from)
}
#[tauri::command]
pub async fn download_and_install(
options: DownloadAndInstallOptions,
on_event: Channel<DownloadEvent>,
) -> Result<DownloadAndInstallResult, CommandError> {
let installer_args = options.installer_args;
let current_dir = options.current_dir;
let update = core_prepare_update(options.update, |event| {
let _ = on_event.send(event);
})
.await
.map_err(CommandError::from)?;
let install = tokio::task::spawn_blocking({
let update = update.clone();
move || {
spawn_installer(
RunInstallerOptions {
installer_path: update.artifact_path.clone(),
args: installer_args,
current_dir,
expected_sha256: Some(update.artifact.sha256.clone()),
},
true,
)
}
})
.await
.map_err(CommandError::from)??;
Ok(DownloadAndInstallResult {
update,
pid: install.pid,
})
}
#[tauri::command]
pub async fn run_installer(
options: RunInstallerOptions,
) -> Result<RunInstallerResult, CommandError> {
tokio::task::spawn_blocking(move || spawn_installer(options, false))
.await
.map_err(CommandError::from)?
}
fn spawn_installer(
options: RunInstallerOptions,
require_expected_hash: bool,
) -> Result<RunInstallerResult, CommandError> {
match &options.expected_sha256 {
Some(expected_sha256) => {
verify_sha256(&options.installer_path, expected_sha256).map_err(CommandError::from)?;
}
None if require_expected_hash => {
return Err(CommandError::from(
"refusing to run installer without an expected SHA-256",
));
}
None => {}
}
let mut command = Command::new(&options.installer_path);
command.args(&options.args);
if let Some(current_dir) = &options.current_dir {
command.current_dir(current_dir);
}
let child = command.spawn().map_err(|error| {
CommandError::from(format!(
"failed to start installer {}: {error}",
options.installer_path.display()
))
})?;
Ok(RunInstallerResult { pid: child.id() })
}