From 935615551784d22a2854264bdc466f5207ba7bb1 Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Tue, 3 Sep 2024 01:02:16 +0300 Subject: [PATCH] feat(updater): allow passing headers & timeout in `Update.download*` methods (#1661) closes #1634 --- .changes/updater-js-headers-download.md | 5 ++++ plugins/updater/api-iife.js | 2 +- plugins/updater/guest-js/index.ts | 26 ++++++++++++++--- plugins/updater/src/commands.rs | 39 +++++++++++++++++++++++-- plugins/updater/src/error.rs | 4 +++ plugins/updater/src/updater.rs | 4 --- 6 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 .changes/updater-js-headers-download.md diff --git a/.changes/updater-js-headers-download.md b/.changes/updater-js-headers-download.md new file mode 100644 index 0000000000..d60533bf11 --- /dev/null +++ b/.changes/updater-js-headers-download.md @@ -0,0 +1,5 @@ +--- +"updater-js": "patch" +--- + +Add a second argument in `Update.download` and `Update.donloadAndInstall` JS APIs to modify headers and timeout when downloading the update. diff --git a/plugins/updater/api-iife.js b/plugins/updater/api-iife.js index a5c53ea05b..2b10e2e707 100644 --- a/plugins/updater/api-iife.js +++ b/plugins/updater/api-iife.js @@ -1 +1 @@ -if("__TAURI__"in window){var __TAURI_PLUGIN_UPDATER__=function(e){"use strict";function t(e,t,s,n){if("a"===s&&!n)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!n:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===s?n:"a"===s?n.call(e):n?n.value:t.get(e)}function s(e,t,s,n,i){if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return t.set(e,s),s}var n,i,r,a;"function"==typeof SuppressedError&&SuppressedError;class o{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,n.set(this,(()=>{})),i.set(this,0),r.set(this,{}),this.id=function(e,t=!1){return window.__TAURI_INTERNALS__.transformCallback(e,t)}((({message:e,id:a})=>{if(a===t(this,i,"f")){s(this,i,a+1),t(this,n,"f").call(this,e);const o=Object.keys(t(this,r,"f"));if(o.length>0){let e=a+1;for(const s of o.sort()){if(parseInt(s)!==e)break;{const i=t(this,r,"f")[s];delete t(this,r,"f")[s],t(this,n,"f").call(this,i),e+=1}}s(this,i,e)}}else t(this,r,"f")[a.toString()]=e}))}set onmessage(e){s(this,n,e)}get onmessage(){return t(this,n,"f")}toJSON(){return`__CHANNEL__:${this.id}`}}async function d(e,t={},s){return window.__TAURI_INTERNALS__.invoke(e,t,s)}n=new WeakMap,i=new WeakMap,r=new WeakMap;class l{get rid(){return t(this,a,"f")}constructor(e){a.set(this,void 0),s(this,a,e)}async close(){return d("plugin:resources|close",{rid:this.rid})}}a=new WeakMap;class c extends l{constructor(e){super(e.rid),this.available=e.available,this.currentVersion=e.currentVersion,this.version=e.version,this.date=e.date,this.body=e.body}async download(e){const t=new o;e&&(t.onmessage=e);const s=await d("plugin:updater|download",{onEvent:t,rid:this.rid});this.downloadedBytes=new l(s)}async install(){if(!this.downloadedBytes)throw new Error("Update.install called before Update.download");await d("plugin:updater|install",{updateRid:this.rid,bytesRid:this.downloadedBytes.rid}),this.downloadedBytes=void 0}async downloadAndInstall(e){const t=new o;e&&(t.onmessage=e),await d("plugin:updater|download_and_install",{onEvent:t,rid:this.rid})}async close(){await(this.downloadedBytes?.close()),await super.close()}}return e.Update=c,e.check=async function(e){return e?.headers&&(e.headers=Array.from(new Headers(e.headers).entries())),await d("plugin:updater|check",{...e}).then((e=>e.available?new c(e):null))},e}({});Object.defineProperty(window.__TAURI__,"updater",{value:__TAURI_PLUGIN_UPDATER__})} +if("__TAURI__"in window){var __TAURI_PLUGIN_UPDATER__=function(e){"use strict";function t(e,t,s,n){if("a"===s&&!n)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!n:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===s?n:"a"===s?n.call(e):n?n.value:t.get(e)}function s(e,t,s,n,i){if("function"==typeof t?e!==t||!i:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");return t.set(e,s),s}var n,i,r,a;"function"==typeof SuppressedError&&SuppressedError;class o{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,n.set(this,(()=>{})),i.set(this,0),r.set(this,{}),this.id=function(e,t=!1){return window.__TAURI_INTERNALS__.transformCallback(e,t)}((({message:e,id:a})=>{if(a===t(this,i,"f")){s(this,i,a+1),t(this,n,"f").call(this,e);const o=Object.keys(t(this,r,"f"));if(o.length>0){let e=a+1;for(const s of o.sort()){if(parseInt(s)!==e)break;{const i=t(this,r,"f")[s];delete t(this,r,"f")[s],t(this,n,"f").call(this,i),e+=1}}s(this,i,e)}}else t(this,r,"f")[a.toString()]=e}))}set onmessage(e){s(this,n,e)}get onmessage(){return t(this,n,"f")}toJSON(){return`__CHANNEL__:${this.id}`}}async function d(e,t={},s){return window.__TAURI_INTERNALS__.invoke(e,t,s)}n=new WeakMap,i=new WeakMap,r=new WeakMap;class l{get rid(){return t(this,a,"f")}constructor(e){a.set(this,void 0),s(this,a,e)}async close(){return d("plugin:resources|close",{rid:this.rid})}}a=new WeakMap;class c extends l{constructor(e){super(e.rid),this.available=e.available,this.currentVersion=e.currentVersion,this.version=e.version,this.date=e.date,this.body=e.body}async download(e,t){const s=new o;e&&(s.onmessage=e);const n=await d("plugin:updater|download",{onEvent:s,rid:this.rid,...t});this.downloadedBytes=new l(n)}async install(){if(!this.downloadedBytes)throw new Error("Update.install called before Update.download");await d("plugin:updater|install",{updateRid:this.rid,bytesRid:this.downloadedBytes.rid}),this.downloadedBytes=void 0}async downloadAndInstall(e,t){const s=new o;e&&(s.onmessage=e),await d("plugin:updater|download_and_install",{onEvent:s,rid:this.rid,...t})}async close(){await(this.downloadedBytes?.close()),await super.close()}}return e.Update=c,e.check=async function(e){return e?.headers&&(e.headers=Array.from(new Headers(e.headers).entries())),await d("plugin:updater|check",{...e}).then((e=>e.available?new c(e):null))},e}({});Object.defineProperty(window.__TAURI__,"updater",{value:__TAURI_PLUGIN_UPDATER__})} diff --git a/plugins/updater/guest-js/index.ts b/plugins/updater/guest-js/index.ts index 1235fa8f7e..42e69716e6 100644 --- a/plugins/updater/guest-js/index.ts +++ b/plugins/updater/guest-js/index.ts @@ -4,14 +4,14 @@ import { invoke, Channel, Resource } from "@tauri-apps/api/core"; -/** Options used to check for updates */ +/** Options used when checking for updates */ interface CheckOptions { /** * Request headers */ headers?: HeadersInit; /** - * Timeout in seconds + * Timeout in milliseconds */ timeout?: number; /** @@ -24,6 +24,18 @@ interface CheckOptions { target?: string; } +/** Options used when downloading an update */ +interface DownloadOptions { + /** + * Request headers + */ + headers?: HeadersInit; + /** + * Timeout in milliseconds + */ + timeout?: number; +} + interface UpdateMetadata { rid: number; available: boolean; @@ -57,7 +69,10 @@ class Update extends Resource { } /** Download the updater package */ - async download(onEvent?: (progress: DownloadEvent) => void): Promise { + async download( + onEvent?: (progress: DownloadEvent) => void, + options?: DownloadOptions, + ): Promise { const channel = new Channel(); if (onEvent) { channel.onmessage = onEvent; @@ -65,6 +80,7 @@ class Update extends Resource { const downloadedBytesRid = await invoke("plugin:updater|download", { onEvent: channel, rid: this.rid, + ...options, }); this.downloadedBytes = new Resource(downloadedBytesRid); } @@ -87,6 +103,7 @@ class Update extends Resource { /** Downloads the updater package and installs it */ async downloadAndInstall( onEvent?: (progress: DownloadEvent) => void, + options?: DownloadOptions, ): Promise { const channel = new Channel(); if (onEvent) { @@ -95,6 +112,7 @@ class Update extends Resource { await invoke("plugin:updater|download_and_install", { onEvent: channel, rid: this.rid, + ...options, }); } @@ -115,5 +133,5 @@ async function check(options?: CheckOptions): Promise { }).then((meta) => (meta.available ? new Update(meta) : null)); } -export type { CheckOptions, DownloadEvent }; +export type { CheckOptions, DownloadOptions, DownloadEvent }; export { check, Update }; diff --git a/plugins/updater/src/commands.rs b/plugins/updater/src/commands.rs index c36376060d..f36a591cf9 100644 --- a/plugins/updater/src/commands.rs +++ b/plugins/updater/src/commands.rs @@ -4,10 +4,11 @@ use crate::{Result, Update, UpdaterExt}; +use http::{HeaderMap, HeaderName, HeaderValue}; use serde::Serialize; use tauri::{ipc::Channel, Manager, Resource, ResourceId, Runtime, Webview}; -use std::time::Duration; +use std::{str::FromStr, time::Duration}; use url::Url; #[derive(Debug, Clone, Serialize)] @@ -53,7 +54,7 @@ pub(crate) async fn check( } } if let Some(timeout) = timeout { - builder = builder.timeout(Duration::from_secs(timeout)); + builder = builder.timeout(Duration::from_millis(timeout)); } if let Some(ref proxy) = proxy { let url = Url::parse(proxy.as_str())?; @@ -83,8 +84,25 @@ pub(crate) async fn download( webview: Webview, rid: ResourceId, on_event: Channel, + headers: Option>, + timeout: Option, ) -> Result { let update = webview.resources_table().get::(rid)?; + + let mut update = (*update).clone(); + + if let Some(headers) = headers { + let mut map = HeaderMap::new(); + for (k, v) in headers { + map.append(HeaderName::from_str(&k)?, HeaderValue::from_str(&v)?); + } + update.headers = map; + } + + if let Some(timeout) = timeout { + update.timeout = Some(Duration::from_millis(timeout)); + } + let mut first_chunk = true; let bytes = update .download( @@ -100,6 +118,7 @@ pub(crate) async fn download( }, ) .await?; + Ok(webview.resources_table().add(DownloadedBytes(bytes))) } @@ -123,9 +142,25 @@ pub(crate) async fn download_and_install( webview: Webview, rid: ResourceId, on_event: Channel, + headers: Option>, + timeout: Option, ) -> Result<()> { let update = webview.resources_table().get::(rid)?; + let mut update = (*update).clone(); + + if let Some(headers) = headers { + let mut map = HeaderMap::new(); + for (k, v) in headers { + map.append(HeaderName::from_str(&k)?, HeaderValue::from_str(&v)?); + } + update.headers = map; + } + + if let Some(timeout) = timeout { + update.timeout = Some(Duration::from_millis(timeout)); + } + let mut first_chunk = true; update diff --git a/plugins/updater/src/error.rs b/plugins/updater/src/error.rs index 2bfadc3f70..ef435cb539 100644 --- a/plugins/updater/src/error.rs +++ b/plugins/updater/src/error.rs @@ -68,6 +68,10 @@ pub enum Error { #[error(transparent)] Http(#[from] http::Error), #[error(transparent)] + InvalidHeaderValue(#[from] http::header::InvalidHeaderValue), + #[error(transparent)] + InvalidHeaderName(#[from] http::header::InvalidHeaderName), + #[error(transparent)] Tauri(#[from] tauri::Error), } diff --git a/plugins/updater/src/updater.rs b/plugins/updater/src/updater.rs index 45d5d25d65..51a1872904 100644 --- a/plugins/updater/src/updater.rs +++ b/plugins/updater/src/updater.rs @@ -469,10 +469,6 @@ impl Update { "Accept", HeaderValue::from_str("application/octet-stream").unwrap(), ); - headers.insert( - "User-Agent", - HeaderValue::from_str("tauri-updater").unwrap(), - ); let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT); if let Some(timeout) = self.timeout {