From 2d731f80224f74faf1b7170b25e04f5da1da49c8 Mon Sep 17 00:00:00 2001 From: Tony <68118705+Legend-Master@users.noreply.github.com> Date: Sat, 29 Mar 2025 08:36:50 +0800 Subject: [PATCH] fix(updater): format `Update.date` to RFC 3339 (#2573) * fix(updater): format `Update.date` to RFC 3339 * Messed up on argument in #2572 * Format * Update example * Avoid extra to_string * Deprecate `Update.available` --- .changes/updater-date-format.md | 6 +++++ examples/api/src/views/Updater.svelte | 10 +++++--- plugins/updater/api-iife.js | 2 +- plugins/updater/guest-js/index.ts | 13 +++++----- plugins/updater/src/commands.rs | 37 ++++++++++++++++----------- plugins/updater/src/error.rs | 2 ++ plugins/updater/src/updater.rs | 3 ++- 7 files changed, 46 insertions(+), 27 deletions(-) create mode 100644 .changes/updater-date-format.md diff --git a/.changes/updater-date-format.md b/.changes/updater-date-format.md new file mode 100644 index 00000000..c7da69e7 --- /dev/null +++ b/.changes/updater-date-format.md @@ -0,0 +1,6 @@ +--- +"updater": "minor:bug" +"updater-js": "minor:bug" +--- + +Fix JS API `Update.date` not formatted to RFC 3339 diff --git a/examples/api/src/views/Updater.svelte b/examples/api/src/views/Updater.svelte index 2fa5e436..26d074a6 100644 --- a/examples/api/src/views/Updater.svelte +++ b/examples/api/src/views/Updater.svelte @@ -12,10 +12,14 @@ isChecking = true try { const update = await check() - onMessage(`Should update: ${update.available}`) - onMessage(update) + if (update) { + onMessage(`Should update: ${update.available}`) + onMessage(update) - newUpdate = update + newUpdate = update + } else { + onMessage('No update available') + } } catch (e) { onMessage(e) } finally { diff --git a/plugins/updater/api-iife.js b/plugins/updater/api-iife.js index 31c81b27..d5403449 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("function"==typeof t||!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||!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,a,r;"function"==typeof SuppressedError&&SuppressedError;const o="__TAURI_TO_IPC_KEY__";class d{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,n.set(this,(()=>{})),i.set(this,0),a.set(this,[]),this.id=function(e,t=!1){return window.__TAURI_INTERNALS__.transformCallback(e,t)}((({message:e,id:r})=>{if(r==t(this,i,"f"))for(t(this,n,"f").call(this,e),s(this,i,t(this,i,"f")+1);t(this,i,"f")in t(this,a,"f");){const e=t(this,a,"f")[t(this,i,"f")];t(this,n,"f").call(this,e),delete t(this,a,"f")[t(this,i,"f")],s(this,i,t(this,i,"f")+1)}else t(this,a,"f")[r]=e}))}set onmessage(e){s(this,n,e)}get onmessage(){return t(this,n,"f")}[(n=new WeakMap,i=new WeakMap,a=new WeakMap,o)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[o]()}}async function l(e,t={},s){return window.__TAURI_INTERNALS__.invoke(e,t,s)}class h{get rid(){return t(this,r,"f")}constructor(e){r.set(this,void 0),s(this,r,e)}async close(){return l("plugin:resources|close",{rid:this.rid})}}r=new WeakMap;class c extends h{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,this.rawJson=e.rawJson}async download(e,t){const s=new d;e&&(s.onmessage=e);const n=await l("plugin:updater|download",{onEvent:s,rid:this.rid,...t});this.downloadedBytes=new h(n)}async install(){if(!this.downloadedBytes)throw new Error("Update.install called before Update.download");await l("plugin:updater|install",{updateRid:this.rid,bytesRid:this.downloadedBytes.rid}),this.downloadedBytes=void 0}async downloadAndInstall(e,t){const s=new d;e&&(s.onmessage=e),await l("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 l("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(t){"use strict";function e(t,e,s,n){if("function"==typeof e||!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===s?n:"a"===s?n.call(t):n?n.value:e.get(t)}function s(t,e,s,n,i){if("function"==typeof e||!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(t,s),s}var n,i,r,a;"function"==typeof SuppressedError&&SuppressedError;const o="__TAURI_TO_IPC_KEY__";class d{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,n.set(this,(()=>{})),i.set(this,0),r.set(this,[]),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}((({message:t,id:a})=>{if(a==e(this,i,"f"))for(e(this,n,"f").call(this,t),s(this,i,e(this,i,"f")+1);e(this,i,"f")in e(this,r,"f");){const t=e(this,r,"f")[e(this,i,"f")];e(this,n,"f").call(this,t),delete e(this,r,"f")[e(this,i,"f")],s(this,i,e(this,i,"f")+1)}else e(this,r,"f")[a]=t}))}set onmessage(t){s(this,n,t)}get onmessage(){return e(this,n,"f")}[(n=new WeakMap,i=new WeakMap,r=new WeakMap,o)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[o]()}}async function c(t,e={},s){return window.__TAURI_INTERNALS__.invoke(t,e,s)}class h{get rid(){return e(this,a,"f")}constructor(t){a.set(this,void 0),s(this,a,t)}async close(){return c("plugin:resources|close",{rid:this.rid})}}a=new WeakMap;class l extends h{constructor(t){super(t.rid),this.available=!0,this.currentVersion=t.currentVersion,this.version=t.version,this.date=t.date,this.body=t.body,this.rawJson=t.rawJson}async download(t,e){const s=new d;t&&(s.onmessage=t);const n=await c("plugin:updater|download",{onEvent:s,rid:this.rid,...e});this.downloadedBytes=new h(n)}async install(){if(!this.downloadedBytes)throw new Error("Update.install called before Update.download");await c("plugin:updater|install",{updateRid:this.rid,bytesRid:this.downloadedBytes.rid}),this.downloadedBytes=void 0}async downloadAndInstall(t,e){const s=new d;t&&(s.onmessage=t),await c("plugin:updater|download_and_install",{onEvent:s,rid:this.rid,...e})}async close(){await(this.downloadedBytes?.close()),await super.close()}}return t.Update=l,t.check=async function(t){t?.headers&&(t.headers=Array.from(new Headers(t.headers).entries()));const e=await c("plugin:updater|check",{...t});return e?new l(e):null},t}({});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 07625eea..87f7929a 100644 --- a/plugins/updater/guest-js/index.ts +++ b/plugins/updater/guest-js/index.ts @@ -38,7 +38,6 @@ interface DownloadOptions { interface UpdateMetadata { rid: number - available: boolean currentVersion: string version: string date?: string @@ -53,6 +52,8 @@ type DownloadEvent = | { event: 'Finished' } class Update extends Resource { + // TODO: remove this field in v3 + /** @deprecated This is always true, check if the return value is `null` instead when using {@linkcode check} */ available: boolean currentVersion: string version: string @@ -63,7 +64,7 @@ class Update extends Resource { constructor(metadata: UpdateMetadata) { super(metadata.rid) - this.available = metadata.available + this.available = true this.currentVersion = metadata.currentVersion this.version = metadata.version this.date = metadata.date @@ -131,12 +132,10 @@ async function check(options?: CheckOptions): Promise { options.headers = Array.from(new Headers(options.headers).entries()) } - return await invoke('plugin:updater|check', { + const metadata = await invoke('plugin:updater|check', { ...options - }).then((meta) => - // TODO: Handle this in the rust side - meta.available ? new Update(meta) : null - ) + }) + return metadata ? new Update(metadata) : null } export type { CheckOptions, DownloadOptions, DownloadEvent } diff --git a/plugins/updater/src/commands.rs b/plugins/updater/src/commands.rs index 55d504bb..ae84294f 100644 --- a/plugins/updater/src/commands.rs +++ b/plugins/updater/src/commands.rs @@ -28,8 +28,7 @@ pub enum DownloadEvent { #[derive(Serialize, Default)] #[serde(rename_all = "camelCase")] pub(crate) struct Metadata { - rid: Option, - available: bool, + rid: ResourceId, current_version: String, version: String, date: Option, @@ -40,8 +39,6 @@ pub(crate) struct Metadata { struct DownloadedBytes(pub Vec); impl Resource for DownloadedBytes {} -// TODO: Align this with the result of `updater.check` to Result> -// and remove `available` instead of handling this in the js side #[tauri::command] pub(crate) async fn check( webview: Webview, @@ -49,7 +46,7 @@ pub(crate) async fn check( timeout: Option, proxy: Option, target: Option, -) -> Result { +) -> Result> { let mut builder = webview.updater_builder(); if let Some(headers) = headers { for (k, v) in headers { @@ -69,18 +66,28 @@ pub(crate) async fn check( let updater = builder.build()?; let update = updater.check().await?; - let mut metadata = Metadata::default(); + if let Some(update) = update { - metadata.available = true; - metadata.current_version.clone_from(&update.current_version); - metadata.version.clone_from(&update.version); - metadata.date = update.date.map(|d| d.to_string()); - metadata.body.clone_from(&update.body); - metadata.raw_json.clone_from(&update.raw_json); - metadata.rid = Some(webview.resources_table().add(update)); + let formatted_date = if let Some(date) = update.date { + let formatted_date = date + .format(&time::format_description::well_known::Rfc3339) + .map_err(|_| crate::Error::FormatDate)?; + Some(formatted_date) + } else { + None + }; + let metadata = Metadata { + current_version: update.current_version.clone(), + version: update.version.clone(), + date: formatted_date, + body: update.body.clone(), + raw_json: update.raw_json.clone(), + rid: webview.resources_table().add(update), + }; + Ok(Some(metadata)) + } else { + Ok(None) } - - Ok(metadata) } #[tauri::command] diff --git a/plugins/updater/src/error.rs b/plugins/updater/src/error.rs index d6d9b0ce..b82e7d55 100644 --- a/plugins/updater/src/error.rs +++ b/plugins/updater/src/error.rs @@ -77,6 +77,8 @@ pub enum Error { InvalidHeaderValue(#[from] http::header::InvalidHeaderValue), #[error(transparent)] InvalidHeaderName(#[from] http::header::InvalidHeaderName), + #[error("Failed to format date")] + FormatDate, /// The configured updater endpoint must use a secure protocol like `https` #[error("The configured updater endpoint must use a secure protocol like `https`.")] InsecureTransportProtocol, diff --git a/plugins/updater/src/updater.rs b/plugins/updater/src/updater.rs index b59272e4..ebd5523c 100644 --- a/plugins/updater/src/updater.rs +++ b/plugins/updater/src/updater.rs @@ -310,7 +310,8 @@ impl UpdaterBuilder { I: IntoIterator, S: Into, { - self.installer_args.extend(args.into_iter().map(Into::into)); + self.current_exe_args + .extend(args.into_iter().map(Into::into)); self } }