parent
c455e9efa0
commit
22039ce791
@ -0,0 +1,70 @@
|
||||
import { invoke, transformCallback } from '@tauri-apps/api/tauri'
|
||||
|
||||
interface CheckOptions {
|
||||
/**
|
||||
* Request headers
|
||||
*/
|
||||
headers?: Record<string, unknown>
|
||||
/**
|
||||
* Timeout in seconds
|
||||
*/
|
||||
timeout?: number
|
||||
/**
|
||||
* Target identifier for the running application. This is sent to the backend.
|
||||
*/
|
||||
target?: string
|
||||
}
|
||||
|
||||
interface UpdateResponse {
|
||||
available: boolean
|
||||
currentVersion: string
|
||||
latestVersion: string
|
||||
date?: string
|
||||
body?: string
|
||||
}
|
||||
|
||||
// TODO: use channel from @tauri-apps/api on v2
|
||||
class Channel<T = unknown> {
|
||||
id: number
|
||||
onmessage: (response: T) => void = () => {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.id = transformCallback((response: T) => {
|
||||
this.onmessage(response)
|
||||
})
|
||||
}
|
||||
|
||||
toJSON(): string {
|
||||
return `__CHANNEL__:${this.id}`
|
||||
}
|
||||
}
|
||||
|
||||
type DownloadEvent =
|
||||
{ event: 'Started', data: { contentLength?: number } } |
|
||||
{ event: 'Progress', data: { chunkLength: number } } |
|
||||
{ event: 'Finished' }
|
||||
|
||||
class Update {
|
||||
response: UpdateResponse
|
||||
|
||||
private constructor(response: UpdateResponse) {
|
||||
this.response = response
|
||||
}
|
||||
|
||||
async downloadAndInstall(onEvent?: (progress: DownloadEvent) => void): Promise<void> {
|
||||
const channel = new Channel<DownloadEvent>()
|
||||
if (onEvent != null) {
|
||||
channel.onmessage = onEvent
|
||||
}
|
||||
return invoke('plugin:updater|download_and_install', { onEvent: channel })
|
||||
}
|
||||
}
|
||||
|
||||
async function check(options?: CheckOptions): Promise<Update> {
|
||||
return invoke('plugin:updater|check', { ...options })
|
||||
}
|
||||
|
||||
export type { CheckOptions, UpdateResponse, DownloadEvent }
|
||||
export { check, Update }
|
@ -1,6 +1,102 @@
|
||||
use tauri::{AppHandle, Runtime};
|
||||
use crate::{PendingUpdate, Result, UpdaterExt};
|
||||
|
||||
use http::header;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use tauri::{api::ipc::Channel, AppHandle, Runtime, State};
|
||||
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct Metadata {
|
||||
available: bool,
|
||||
current_version: String,
|
||||
latest_version: String,
|
||||
date: Option<String>,
|
||||
body: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct HeaderMap(header::HeaderMap);
|
||||
|
||||
impl<'de> Deserialize<'de> for HeaderMap {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let map = HashMap::<String, String>::deserialize(deserializer)?;
|
||||
let mut headers = header::HeaderMap::default();
|
||||
for (key, value) in map {
|
||||
if let (Ok(key), Ok(value)) = (
|
||||
header::HeaderName::from_bytes(key.as_bytes()),
|
||||
header::HeaderValue::from_str(&value),
|
||||
) {
|
||||
headers.insert(key, value);
|
||||
} else {
|
||||
return Err(serde::de::Error::custom(format!(
|
||||
"invalid header `{key}` `{value}`"
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(Self(headers))
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn check<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
pending: State<'_, PendingUpdate<R>>,
|
||||
headers: Option<HeaderMap>,
|
||||
timeout: Option<u64>,
|
||||
target: Option<String>,
|
||||
) -> Result<Metadata> {
|
||||
let mut builder = app.updater();
|
||||
if let Some(headers) = headers {
|
||||
for (k, v) in headers.0.iter() {
|
||||
builder = builder.header(k, v)?;
|
||||
}
|
||||
}
|
||||
if let Some(timeout) = timeout {
|
||||
builder = builder.timeout(Duration::from_secs(timeout));
|
||||
}
|
||||
if let Some(target) = target {
|
||||
builder = builder.target(target);
|
||||
}
|
||||
|
||||
let response = builder.check().await?;
|
||||
|
||||
let metadata = Metadata {
|
||||
available: response.is_update_available(),
|
||||
current_version: response.current_version().to_string(),
|
||||
latest_version: response.latest_version().to_string(),
|
||||
date: response.date().map(|d| d.to_string()),
|
||||
body: response.body().cloned(),
|
||||
};
|
||||
|
||||
pending.0.lock().await.replace(response);
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct DownloadProgress {
|
||||
chunk_length: usize,
|
||||
content_length: Option<u64>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn version<R: Runtime>(app: AppHandle<R>) -> String {
|
||||
app.package_info().version.to_string()
|
||||
pub(crate) async fn download_and_install<R: Runtime>(
|
||||
_app: AppHandle<R>,
|
||||
pending: State<'_, PendingUpdate<R>>,
|
||||
on_event: Channel<R>,
|
||||
) -> Result<()> {
|
||||
if let Some(pending) = pending.0.lock().await.take() {
|
||||
pending
|
||||
.download_and_install(move |event| {
|
||||
let _ = on_event.send(&event);
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
Loading…
Reference in new issue