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]
|
#[tauri::command]
|
||||||
pub fn version<R: Runtime>(app: AppHandle<R>) -> String {
|
pub(crate) async fn download_and_install<R: Runtime>(
|
||||||
app.package_info().version.to_string()
|
_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