feat(desktop): portable phase-2 app loader infra (#5341)
This implements backend path management, backup system, cross-platform utilities, and refactors the `appload` plugin arch to support portable mode deployment. The changes are mainly establishing foundational infra maintaining current frontend behavior until phase-3+ integration.
This commit is contained in:
parent
9504369ce1
commit
f234e66078
44 changed files with 2914 additions and 1027 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "hoppscotch-agent",
|
||||
"private": true,
|
||||
"version": "0.1.12",
|
||||
"version": "0.1.13",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
|
|
|||
2
packages/hoppscotch-agent/src-tauri/Cargo.lock
generated
2
packages/hoppscotch-agent/src-tauri/Cargo.lock
generated
|
|
@ -2076,7 +2076,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "hoppscotch-agent"
|
||||
version = "0.1.12"
|
||||
version = "0.1.13"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"axum",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "hoppscotch-agent"
|
||||
version = "0.1.12"
|
||||
version = "0.1.13"
|
||||
description = "A cross-platform HTTP request agent for Hoppscotch for advanced request handling including custom headers, certificates, proxies, and local system integration."
|
||||
authors = ["AndrewBastin", "CuriousCorrelation"]
|
||||
edition = "2021"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "Hoppscotch Agent",
|
||||
"version": "0.1.12",
|
||||
"version": "0.1.13",
|
||||
"identifier": "io.hoppscotch.agent",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "Hoppscotch Agent Portable",
|
||||
"version": "0.1.12",
|
||||
"version": "0.1.13",
|
||||
"identifier": "io.hoppscotch.agent",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
"@fontsource-variable/roboto-mono": "5.2.6",
|
||||
"@hoppscotch/common": "workspace:^",
|
||||
"@hoppscotch/kernel": "workspace:^",
|
||||
"@hoppscotch/plugin-appload": "github:CuriousCorrelation/tauri-plugin-appload#e8dbe06eabf947e5efaf07d2e573238ceb11a7b1",
|
||||
"@hoppscotch/plugin-appload": "github:CuriousCorrelation/tauri-plugin-appload#feat-desktop-appload-top-level-config",
|
||||
"@hoppscotch/ui": "0.2.5",
|
||||
"@tauri-apps/api": "2.1.1",
|
||||
"@tauri-apps/plugin-fs": "2.0.2",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -14,7 +14,7 @@ serde = "1.0"
|
|||
serde_json = { version = "1", features = [] }
|
||||
thiserror = { version = "2.0.3", features = [] }
|
||||
url = { version = "2.5.3", features = ["serde"] }
|
||||
reqwest = { version = "0.12.9", features = [] }
|
||||
reqwest = { version = "0.12.9", features = ["json"] }
|
||||
zip = { version = "2.2.0", features = [] }
|
||||
tokio = { version = "1.41.1", features = [] }
|
||||
mime-infer = { version = "3.0.0", features = [] }
|
||||
|
|
@ -35,6 +35,7 @@ mime_guess = "2.0.5"
|
|||
rayon = "1.10.0"
|
||||
hex_color = "3.0.0"
|
||||
dunce = "1.0.5"
|
||||
bon = "3.6.3"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-plugin = { version = "2.0.1", features = ["build"] }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const COMMANDS: &[&str] = &["load", "download", "clear", "remove"];
|
||||
const COMMANDS: &[&str] = &["load", "download", "clear", "close", "remove"];
|
||||
|
||||
fn main() {
|
||||
tauri_plugin::Builder::new(COMMANDS)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ async function download(options) {
|
|||
async function load(options) {
|
||||
return await core.invoke('plugin:appload|load', { options });
|
||||
}
|
||||
async function close(options) {
|
||||
return await core.invoke('plugin:appload|close', { options });
|
||||
}
|
||||
async function remove(options) {
|
||||
return await core.invoke('plugin:appload|remove', { options });
|
||||
}
|
||||
|
|
@ -16,6 +19,7 @@ async function clear() {
|
|||
}
|
||||
|
||||
exports.clear = clear;
|
||||
exports.close = close;
|
||||
exports.download = download;
|
||||
exports.load = load;
|
||||
exports.remove = remove;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,12 @@ export interface LoadResponse {
|
|||
success: boolean;
|
||||
windowLabel: string;
|
||||
}
|
||||
export interface CloseOptions {
|
||||
windowLabel: string;
|
||||
}
|
||||
export interface CloseResponse {
|
||||
success: boolean;
|
||||
}
|
||||
export interface RemoveOptions {
|
||||
bundleName: string;
|
||||
serverUrl: string;
|
||||
|
|
@ -32,6 +38,7 @@ export interface RemoveResponse {
|
|||
}
|
||||
export declare function download(options: DownloadOptions): Promise<DownloadResponse>;
|
||||
export declare function load(options: LoadOptions): Promise<LoadResponse>;
|
||||
export declare function close(options: CloseOptions): Promise<CloseResponse>;
|
||||
export declare function remove(options: RemoveOptions): Promise<RemoveResponse>;
|
||||
export declare function clear(): Promise<void>;
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["guest-js/index.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,MAAM,CAAC,EAAE,aAAa,CAAA;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAElF;AAED,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAEtE;AAED,wBAAsB,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,CAE5E;AAED,wBAAsB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3C"}
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["guest-js/index.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,MAAM,CAAC,EAAE,aAAa,CAAA;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAElF;AAED,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAEtE;AAED,wBAAsB,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CAEzE;AAED,wBAAsB,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,CAE5E;AAED,wBAAsB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3C"}
|
||||
|
|
@ -6,6 +6,9 @@ async function download(options) {
|
|||
async function load(options) {
|
||||
return await invoke('plugin:appload|load', { options });
|
||||
}
|
||||
async function close(options) {
|
||||
return await invoke('plugin:appload|close', { options });
|
||||
}
|
||||
async function remove(options) {
|
||||
return await invoke('plugin:appload|remove', { options });
|
||||
}
|
||||
|
|
@ -13,4 +16,4 @@ async function clear() {
|
|||
return await invoke('plugin:appload|clear');
|
||||
}
|
||||
|
||||
export { clear, download, load, remove };
|
||||
export { clear, close, download, load, remove };
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.1.1",
|
||||
"@tauri-apps/api": "2.1.1",
|
||||
"tauri-plugin-appload-api": "file:../../"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,14 @@ export interface LoadResponse {
|
|||
windowLabel: string
|
||||
}
|
||||
|
||||
export interface CloseOptions {
|
||||
windowLabel: string
|
||||
}
|
||||
|
||||
export interface CloseResponse {
|
||||
success: boolean
|
||||
}
|
||||
|
||||
export interface RemoveOptions {
|
||||
bundleName: string
|
||||
serverUrl: string
|
||||
|
|
@ -47,6 +55,10 @@ export async function load(options: LoadOptions): Promise<LoadResponse> {
|
|||
return await invoke<LoadResponse>('plugin:appload|load', { options })
|
||||
}
|
||||
|
||||
export async function close(options: CloseOptions): Promise<CloseResponse> {
|
||||
return await invoke<CloseResponse>('plugin:appload|close', { options })
|
||||
}
|
||||
|
||||
export async function remove(options: RemoveOptions): Promise<RemoveResponse> {
|
||||
return await invoke<RemoveResponse>('plugin:appload|remove', { options })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
# Automatically generated - DO NOT EDIT!
|
||||
|
||||
"$schema" = "../../schemas/schema.json"
|
||||
|
||||
[[permission]]
|
||||
identifier = "allow-close"
|
||||
description = "Enables the close command without any pre-configured scope."
|
||||
commands.allow = ["close"]
|
||||
|
||||
[[permission]]
|
||||
identifier = "deny-close"
|
||||
description = "Denies the close command without any pre-configured scope."
|
||||
commands.deny = ["close"]
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
Default permissions for AppLoad plugin
|
||||
|
||||
#### This default permission set includes the following:
|
||||
|
||||
- `allow-load`
|
||||
- `allow-download`
|
||||
- `allow-clear`
|
||||
|
|
@ -45,6 +47,32 @@ Denies the clear command without any pre-configured scope.
|
|||
<tr>
|
||||
<td>
|
||||
|
||||
`appload:allow-close`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Enables the close command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`appload:deny-close`
|
||||
|
||||
</td>
|
||||
<td>
|
||||
|
||||
Denies the close command without any pre-configured scope.
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
|
||||
`appload:allow-download`
|
||||
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
[default]
|
||||
description = "Default permissions for AppLoad plugin"
|
||||
permissions = ["allow-load", "allow-download", "allow-clear", "allow-remove"]
|
||||
permissions = ["allow-load", "allow-download", "allow-clear", "allow-close", "allow-remove"]
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
"minimum": 1.0
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does. Tauri convention is to use <h4> headings in markdown content for Tauri documentation generation purposes.",
|
||||
"description": "Human-readable description of what the permission does. Tauri convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
|
|
@ -111,7 +111,7 @@
|
|||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"description": "Human-readable description of what the permission does. Tauri internal convention is to use <h4> headings in markdown content for Tauri documentation generation purposes.",
|
||||
"description": "Human-readable description of what the permission does. Tauri internal convention is to use `<h4>` headings in markdown content for Tauri documentation generation purposes.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
|
|
@ -297,57 +297,78 @@
|
|||
{
|
||||
"description": "Enables the clear command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-clear"
|
||||
"const": "allow-clear",
|
||||
"markdownDescription": "Enables the clear command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the clear command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-clear"
|
||||
"const": "deny-clear",
|
||||
"markdownDescription": "Denies the clear command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the close command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-close"
|
||||
},
|
||||
{
|
||||
"description": "Denies the close command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-close"
|
||||
},
|
||||
{
|
||||
"description": "Enables the download command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-download"
|
||||
"const": "allow-download",
|
||||
"markdownDescription": "Enables the download command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the download command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-download"
|
||||
"const": "deny-download",
|
||||
"markdownDescription": "Denies the download command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the load command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-load"
|
||||
"const": "allow-load",
|
||||
"markdownDescription": "Enables the load command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the load command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-load"
|
||||
"const": "deny-load",
|
||||
"markdownDescription": "Denies the load command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the ping command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-ping"
|
||||
"const": "allow-ping",
|
||||
"markdownDescription": "Enables the ping command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the ping command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-ping"
|
||||
"const": "deny-ping",
|
||||
"markdownDescription": "Denies the ping command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the remove command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "allow-remove"
|
||||
"const": "allow-remove",
|
||||
"markdownDescription": "Enables the remove command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the remove command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "deny-remove"
|
||||
"const": "deny-remove",
|
||||
"markdownDescription": "Denies the remove command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Default permissions for AppLoad plugin",
|
||||
"description": "Default permissions for AppLoad plugin\n#### This default permission set includes:\n\n- `allow-load`\n- `allow-download`\n- `allow-clear`\n- `allow-remove`",
|
||||
"type": "string",
|
||||
"const": "default"
|
||||
"const": "default",
|
||||
"markdownDescription": "Default permissions for AppLoad plugin\n#### This default permission set includes:\n\n- `allow-load`\n- `allow-download`\n- `allow-clear`\n- `allow-remove`"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ importers:
|
|||
.:
|
||||
dependencies:
|
||||
'@tauri-apps/api':
|
||||
specifier: ^2.0.0
|
||||
specifier: 2.1.1
|
||||
version: 2.1.1
|
||||
devDependencies:
|
||||
'@rollup/plugin-typescript':
|
||||
specifier: ^11.1.6
|
||||
version: 11.1.6(rollup@4.28.0)(tslib@2.8.1)(typescript@5.8.3)
|
||||
version: 11.1.6(rollup@4.28.0)(tslib@2.8.1)(typescript@5.9.2)
|
||||
rollup:
|
||||
specifier: ^4.9.6
|
||||
version: 4.28.0
|
||||
|
|
@ -22,8 +22,8 @@ importers:
|
|||
specifier: ^2.6.2
|
||||
version: 2.8.1
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
specifier: 5.9.2
|
||||
version: 5.9.2
|
||||
|
||||
packages:
|
||||
|
||||
|
|
@ -187,18 +187,18 @@ packages:
|
|||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
typescript@5.8.3:
|
||||
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
|
||||
typescript@5.9.2:
|
||||
resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@rollup/plugin-typescript@11.1.6(rollup@4.28.0)(tslib@2.8.1)(typescript@5.8.3)':
|
||||
'@rollup/plugin-typescript@11.1.6(rollup@4.28.0)(tslib@2.8.1)(typescript@5.9.2)':
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.1.3(rollup@4.28.0)
|
||||
resolve: 1.22.8
|
||||
typescript: 5.8.3
|
||||
typescript: 5.9.2
|
||||
optionalDependencies:
|
||||
rollup: 4.28.0
|
||||
tslib: 2.8.1
|
||||
|
|
@ -322,4 +322,4 @@ snapshots:
|
|||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
typescript@5.8.3: {}
|
||||
typescript@5.9.2: {}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ use tauri::{
|
|||
use crate::{
|
||||
bundle::BundleLoader,
|
||||
cache::CacheManager,
|
||||
models::{DownloadOptions, DownloadResponse, LoadOptions, LoadResponse},
|
||||
models::{
|
||||
CloseOptions, CloseResponse, DownloadOptions, DownloadResponse, LoadOptions, LoadResponse,
|
||||
},
|
||||
storage::{StorageError, StorageManager},
|
||||
ui, RemoveOptions, RemoveResponse, Result,
|
||||
};
|
||||
|
|
@ -111,21 +113,6 @@ pub async fn load<R: Runtime>(app: AppHandle<R>, options: LoadOptions) -> Result
|
|||
})?;
|
||||
}
|
||||
|
||||
for old_label in [
|
||||
&format!("{}-curr", base_label),
|
||||
&format!("{}-next", base_label),
|
||||
"main",
|
||||
] {
|
||||
if old_label == &label {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(old_window) = app.get_webview_window(old_label) {
|
||||
old_window.close()?;
|
||||
tracing::info!("Closing {} window", old_label);
|
||||
}
|
||||
}
|
||||
|
||||
let response = LoadResponse {
|
||||
success: window.is_visible().unwrap_or(false),
|
||||
window_label: label,
|
||||
|
|
@ -135,6 +122,26 @@ pub async fn load<R: Runtime>(app: AppHandle<R>, options: LoadOptions) -> Result
|
|||
Ok(response)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn close<R: Runtime>(app: AppHandle<R>, options: CloseOptions) -> Result<CloseResponse> {
|
||||
tracing::info!(?options, "Starting window close process");
|
||||
|
||||
let Some(window) = app.get_webview_window(&options.window_label) else {
|
||||
tracing::info!(window_label = %options.window_label, "Window not found or already closed");
|
||||
return Ok(CloseResponse { success: true });
|
||||
};
|
||||
|
||||
window.close().map_err(|e| {
|
||||
tracing::error!(?e, window_label = %options.window_label, "Failed to close window");
|
||||
e
|
||||
})?;
|
||||
|
||||
let response = CloseResponse { success: true };
|
||||
|
||||
tracing::info!(?response, "Window close process completed");
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn remove<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
mod model;
|
||||
|
||||
pub use model::Config;
|
||||
pub use model::{ApiConfig, CacheConfig, Config, StorageConfig};
|
||||
|
||||
pub const DEFAULT_CONFIG_PATH: &str = "config.json";
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
use bon::Builder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::cache::DEFAULT_CACHE_SIZE;
|
||||
use crate::vendor::VendorConfig;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Builder, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub api: ApiConfig,
|
||||
|
|
@ -12,6 +14,8 @@ pub struct Config {
|
|||
pub cache: CacheConfig,
|
||||
#[serde(default)]
|
||||
pub storage: StorageConfig,
|
||||
#[serde(skip)]
|
||||
pub vendor: VendorConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -35,16 +39,6 @@ pub struct StorageConfig {
|
|||
pub max_bundle_size: usize,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
api: ApiConfig::default(),
|
||||
cache: CacheConfig::default(),
|
||||
storage: StorageConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ApiConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ use tauri::{
|
|||
Manager, Runtime,
|
||||
};
|
||||
|
||||
pub use config::Config;
|
||||
pub use config::{ApiConfig, CacheConfig, StorageConfig};
|
||||
pub use models::*;
|
||||
|
||||
#[cfg(desktop)]
|
||||
|
|
@ -40,35 +42,28 @@ mod vendor;
|
|||
mod verification;
|
||||
|
||||
pub use error::{Error, Result};
|
||||
pub use vendor::{VendorConfig, VendorConfigBuilder};
|
||||
pub use vendor::VendorConfig;
|
||||
|
||||
#[cfg(mobile)]
|
||||
use mobile::Appload;
|
||||
|
||||
const KERNEL_JS: &str = include_str!("kernel.js");
|
||||
|
||||
pub fn init<R: Runtime>(vendor: VendorConfigBuilder) -> TauriPlugin<R> {
|
||||
pub fn init<R: Runtime>(config: Config) -> TauriPlugin<R> {
|
||||
Builder::new("appload")
|
||||
.setup(move |app, api| {
|
||||
tracing::info!("Initializing appload plugin");
|
||||
|
||||
tracing::debug!("Loading configuration settings.");
|
||||
let mut config = config::Config::default();
|
||||
|
||||
tracing::debug!("Resolving app config directory for storage root.");
|
||||
let app_config_dir = app.path().app_config_dir().map_err(|e| {
|
||||
tracing::error!(error = %e, "Failed to resolve app config directory.");
|
||||
Error::Config(e.to_string())
|
||||
})?;
|
||||
tracing::debug!("Using provided configuration settings.");
|
||||
let storage_root = config.storage.root_dir.clone();
|
||||
|
||||
tracing::info!(
|
||||
path = ?app_config_dir,
|
||||
"Setting storage root to app config directory."
|
||||
path = ?storage_root,
|
||||
"Using configured storage root directory."
|
||||
);
|
||||
config.storage.root_dir = app_config_dir;
|
||||
|
||||
let storage = tauri::async_runtime::block_on(async {
|
||||
let storage = storage::StorageManager::new(config.storage.root_dir.clone())
|
||||
let storage = storage::StorageManager::new(storage_root)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!(error = %e, "Failed to initialize storage manager");
|
||||
|
|
@ -107,15 +102,8 @@ pub fn init<R: Runtime>(vendor: VendorConfigBuilder) -> TauriPlugin<R> {
|
|||
let cache = cache.clone();
|
||||
let storage = storage.clone();
|
||||
tauri::async_runtime::block_on(async move {
|
||||
match vendor.build() {
|
||||
Ok(vendor) => {
|
||||
if let Err(e) = vendor.initialize(tauri_config, cache, storage).await {
|
||||
tracing::error!(error = %e, "Failed to initialize vendor.");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Failed to build vendor.");
|
||||
}
|
||||
if let Err(e) = config.vendor.initialize(tauri_config, cache, storage).await {
|
||||
tracing::error!(error = %e, "Failed to initialize vendor.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -142,6 +130,7 @@ pub fn init<R: Runtime>(vendor: VendorConfigBuilder) -> TauriPlugin<R> {
|
|||
.invoke_handler(tauri::generate_handler![
|
||||
commands::download,
|
||||
commands::load,
|
||||
commands::close,
|
||||
commands::remove,
|
||||
commands::clear
|
||||
])
|
||||
|
|
|
|||
|
|
@ -160,6 +160,18 @@ pub struct LoadResponse {
|
|||
pub window_label: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CloseOptions {
|
||||
pub window_label: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CloseResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoveOptions {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tauri::Config;
|
||||
use tauri::Config as TauriConfig;
|
||||
|
||||
use crate::{
|
||||
bundle::VerifiedBundle, cache::CacheManager, storage::StorageManager, vendor::VendorError,
|
||||
|
|
@ -11,22 +13,26 @@ use super::Result;
|
|||
|
||||
const VENDOR_SOURCE: &str = "vendor";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct VendorConfig {
|
||||
pub(super) bundle: Option<(Vec<u8>, Manifest)>,
|
||||
pub bundle_path: PathBuf,
|
||||
pub manifest_path: PathBuf,
|
||||
}
|
||||
|
||||
impl VendorConfig {
|
||||
pub(crate) async fn initialize(
|
||||
self,
|
||||
config: Config,
|
||||
pub async fn initialize(
|
||||
&self,
|
||||
config: TauriConfig,
|
||||
cache: Arc<CacheManager>,
|
||||
storage: Arc<StorageManager>,
|
||||
) -> Result<()> {
|
||||
let Some((content, manifest)) = self.bundle else {
|
||||
tracing::info!("No vendored bundle provided, skipping initialization");
|
||||
return Ok(());
|
||||
};
|
||||
let content = fs::read(&self.bundle_path).map_err(|e| VendorError::Io(e))?;
|
||||
|
||||
let manifest_str =
|
||||
fs::read_to_string(&self.manifest_path).map_err(|e| VendorError::Io(e))?;
|
||||
|
||||
let manifest: Manifest =
|
||||
serde_json::from_str(&manifest_str).map_err(|e| VendorError::Json(e))?;
|
||||
|
||||
let max_bundle_size = 100 * 1024 * 1024;
|
||||
if content.len() > max_bundle_size {
|
||||
|
|
@ -55,6 +61,9 @@ impl VendorConfig {
|
|||
|
||||
let verified = VerifiedBundle::trust(content, manifest)?;
|
||||
|
||||
// NOTE: This is temporary, to make sure bundle verifier
|
||||
// has required file at the location,
|
||||
// won't be necessary after source refactor.
|
||||
storage
|
||||
.store_bundle(&name, VENDOR_SOURCE, &version, &verified)
|
||||
.await?;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
mod builder;
|
||||
mod config;
|
||||
mod error;
|
||||
|
||||
pub use builder::VendorConfigBuilder;
|
||||
pub use config::VendorConfig;
|
||||
pub use error::{Result, VendorError};
|
||||
|
|
|
|||
164
packages/hoppscotch-desktop/src-tauri/Cargo.lock
generated
164
packages/hoppscotch-desktop/src-tauri/Cargo.lock
generated
|
|
@ -251,7 +251,7 @@ dependencies = [
|
|||
"futures-lite",
|
||||
"parking",
|
||||
"polling",
|
||||
"rustix",
|
||||
"rustix 0.38.42",
|
||||
"slab",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
|
|
@ -283,7 +283,7 @@ dependencies = [
|
|||
"cfg-if",
|
||||
"event-listener",
|
||||
"futures-lite",
|
||||
"rustix",
|
||||
"rustix 0.38.42",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
|
|
@ -310,7 +310,7 @@ dependencies = [
|
|||
"cfg-if",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"rustix",
|
||||
"rustix 0.38.42",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"windows-sys 0.59.0",
|
||||
|
|
@ -521,6 +521,31 @@ dependencies = [
|
|||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bon"
|
||||
version = "3.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "537c317ddf588aab15c695bf92cf55dec159b93221c074180ca3e0e5a94da415"
|
||||
dependencies = [
|
||||
"bon-macros",
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bon-macros"
|
||||
version = "3.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca5abbf2d4a4c6896197c9de13d6d7cb7eff438c63dacde1dde980569cb00248"
|
||||
dependencies = [
|
||||
"darling 0.21.2",
|
||||
"ident_case",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "7.0.0"
|
||||
|
|
@ -1136,8 +1161,18 @@ version = "0.20.10"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
"darling_core 0.20.10",
|
||||
"darling_macro 0.20.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08440b3dd222c3d0433e63e097463969485f112baff337dfdaca043a0d760570"
|
||||
dependencies = [
|
||||
"darling_core 0.21.2",
|
||||
"darling_macro 0.21.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1154,13 +1189,38 @@ dependencies = [
|
|||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d25b7912bc28a04ab1b7715a68ea03aaa15662b43a1a4b2c480531fd19f8bf7e"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_core 0.20.10",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce154b9bea7fb0c8e8326e62d00354000c36e79770ff21b8c84e3aa267d9d531"
|
||||
dependencies = [
|
||||
"darling_core 0.21.2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
|
@ -1967,6 +2027,18 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.1"
|
||||
|
|
@ -2884,6 +2956,12 @@ version = "0.4.14"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.7.4"
|
||||
|
|
@ -3929,7 +4007,7 @@ dependencies = [
|
|||
"concurrent-queue",
|
||||
"hermit-abi",
|
||||
"pin-project-lite",
|
||||
"rustix",
|
||||
"rustix 0.38.42",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
|
@ -3955,6 +4033,16 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "1.3.1"
|
||||
|
|
@ -4117,6 +4205,12 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.7.3"
|
||||
|
|
@ -4477,7 +4571,20 @@ dependencies = [
|
|||
"bitflags 2.6.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"linux-raw-sys 0.4.14",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.9.4",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
|
|
@ -4765,7 +4872,7 @@ version = "3.11.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.20.10",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.90",
|
||||
|
|
@ -5341,10 +5448,11 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "tauri-plugin-appload"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/CuriousCorrelation/tauri-plugin-appload?rev=e8dbe06eabf947e5efaf07d2e573238ceb11a7b1#e8dbe06eabf947e5efaf07d2e573238ceb11a7b1"
|
||||
source = "git+https://github.com/CuriousCorrelation/tauri-plugin-appload?branch=feat-desktop-appload-top-level-config#3d03f6c7b1726ecf179af2124e77a9742d600637"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"blake3",
|
||||
"bon",
|
||||
"chrono",
|
||||
"cocoa 0.26.0",
|
||||
"dashmap",
|
||||
|
|
@ -5681,14 +5789,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.14.0"
|
||||
version = "3.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
|
||||
checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"rustix 1.0.8",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
|
|
@ -6351,6 +6459,15 @@ version = "0.11.0+wasi-snapshot-preview1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.2+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.99"
|
||||
|
|
@ -6439,7 +6556,7 @@ checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6"
|
|||
dependencies = [
|
||||
"cc",
|
||||
"downcast-rs",
|
||||
"rustix",
|
||||
"rustix 0.38.42",
|
||||
"scoped-tls",
|
||||
"smallvec",
|
||||
"wayland-sys",
|
||||
|
|
@ -6452,7 +6569,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"rustix",
|
||||
"rustix 0.38.42",
|
||||
"wayland-backend",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
|
@ -6619,7 +6736,7 @@ dependencies = [
|
|||
"either",
|
||||
"home",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"rustix 0.38.42",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -7093,6 +7210,15 @@ dependencies = [
|
|||
"windows 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "write16"
|
||||
version = "1.0.0"
|
||||
|
|
@ -7176,8 +7302,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"rustix",
|
||||
"linux-raw-sys 0.4.14",
|
||||
"rustix 0.38.42",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ tauri-plugin-store = "2.2.0"
|
|||
tauri-plugin-dialog = "2.2.0"
|
||||
tauri-plugin-fs = "2.2.0"
|
||||
tauri-plugin-deep-link = "2.2.0"
|
||||
tauri-plugin-appload = { git = "https://github.com/CuriousCorrelation/tauri-plugin-appload", rev = "e8dbe06eabf947e5efaf07d2e573238ceb11a7b1" }
|
||||
tauri-plugin-appload = { git = "https://github.com/CuriousCorrelation/tauri-plugin-appload", branch = "feat-desktop-appload-top-level-config" }
|
||||
tauri-plugin-relay = { git = "https://github.com/CuriousCorrelation/tauri-plugin-relay", rev = "ff18f776ddeb53dbbdeaf97e1fabc30bdc57c158" }
|
||||
axum = "0.8.1"
|
||||
tower-http = { version = "0.6.2", features = ["cors"] }
|
||||
|
|
@ -44,6 +44,9 @@ tauri-plugin-http = { version = "2.0.1", features = ["gzip"] }
|
|||
tauri-plugin-opener = "2"
|
||||
semver = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.20.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
tempfile = { version = "3.13.0" }
|
||||
winreg = { version = "0.52.0" }
|
||||
|
|
|
|||
|
|
@ -16,8 +16,22 @@
|
|||
"dialog:default",
|
||||
"process:default",
|
||||
"updater:default",
|
||||
"fs:allow-copy-file",
|
||||
"fs:allow-remove",
|
||||
"fs:allow-exists",
|
||||
"fs:allow-read-file",
|
||||
"fs:allow-read-dir",
|
||||
"fs:allow-write-file",
|
||||
"fs:allow-write-text-file",
|
||||
{
|
||||
"identifier": "fs:scope",
|
||||
"allow": [
|
||||
{ "path": "$APPCONFIG" },
|
||||
{ "path": "$APPCONFIG/**" },
|
||||
{ "path": "$APPDATA" },
|
||||
{ "path": "$APPDATA/**" }
|
||||
]
|
||||
},
|
||||
"deep-link:default",
|
||||
"appload:default",
|
||||
"relay:default"
|
||||
|
|
|
|||
425
packages/hoppscotch-desktop/src-tauri/src/backup.rs
Normal file
425
packages/hoppscotch-desktop/src-tauri/src/backup.rs
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use semver::Version;
|
||||
use tauri::{AppHandle, Runtime};
|
||||
|
||||
use crate::{error::HoppError, path};
|
||||
|
||||
const MAX_BACKUP_COUNT: usize = 3;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_and_backup_on_version_change<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
) -> Result<(), String> {
|
||||
let handle = app.clone();
|
||||
match tauri::async_runtime::spawn_blocking(move || perform_version_check_and_backup(handle))
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => Ok(()),
|
||||
Ok(Err(e)) => {
|
||||
tracing::error!(error = %e, "Backup operation failed");
|
||||
Err(e.to_string())
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Backup task panicked");
|
||||
Err("Backup operation panicked".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn perform_version_check_and_backup<R: Runtime>(app: AppHandle<R>) -> Result<(), HoppError> {
|
||||
let current_version = get_current_app_version(&app)?;
|
||||
|
||||
tracing::info!(
|
||||
current_version = %current_version,
|
||||
"Version check initiated"
|
||||
);
|
||||
|
||||
if !backup_exists_for_version(¤t_version)? {
|
||||
tracing::info!(
|
||||
version = %current_version,
|
||||
"No backup found for current version, creating backup"
|
||||
);
|
||||
|
||||
backup_current_data(¤t_version)?;
|
||||
cleanup_old_backups()?;
|
||||
|
||||
tracing::info!("Backup operation completed successfully");
|
||||
} else {
|
||||
tracing::debug!(
|
||||
version = %current_version,
|
||||
"Backup already exists for current version, skipping"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_current_app_version<R: Runtime>(app: &AppHandle<R>) -> Result<Version, HoppError> {
|
||||
let version_str = app.package_info().version.to_string();
|
||||
Version::parse(&version_str).map_err(|e| {
|
||||
tracing::error!(
|
||||
version_string = %version_str,
|
||||
error = %e,
|
||||
"Failed to parse current app version"
|
||||
);
|
||||
HoppError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("Invalid version format: {}", version_str),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn backup_exists_for_version(version: &Version) -> Result<bool, HoppError> {
|
||||
let backup_dir = path::backup_dir()?;
|
||||
let version_backup_dir = backup_dir.join(format!("backup-by-v{}", version));
|
||||
|
||||
Ok(version_backup_dir.exists())
|
||||
}
|
||||
|
||||
fn backup_current_data(current_version: &Version) -> Result<(), HoppError> {
|
||||
let config_dir = path::config_dir()?;
|
||||
let backup_dir = path::backup_dir()?;
|
||||
let version_backup_dir = backup_dir.join(format!("backup-by-v{}", current_version));
|
||||
|
||||
if !config_dir.exists() {
|
||||
tracing::warn!("Config directory doesn't exist, skipping backup");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
source = %config_dir.display(),
|
||||
target = %version_backup_dir.display(),
|
||||
"Starting full config directory backup"
|
||||
);
|
||||
|
||||
fs::create_dir_all(&version_backup_dir)?;
|
||||
|
||||
// NOTE: This copies all contents of `config_dir` to `version_backup_dir`,
|
||||
// but excludes the `backup` and `latest` directories to avoid infinite recursion
|
||||
// and prevent backing up the current working data that might be in flux.
|
||||
copy_directory_contents_excluding_special_dirs(&config_dir, &version_backup_dir, &backup_dir)?;
|
||||
|
||||
tracing::info!(
|
||||
target = %version_backup_dir.display(),
|
||||
"Full config backup completed successfully"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_directory_contents_excluding_special_dirs(
|
||||
src: &PathBuf,
|
||||
dst: &PathBuf,
|
||||
backup_dir: &PathBuf,
|
||||
) -> Result<(), HoppError> {
|
||||
if !src.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let latest_dir = match path::latest_dir() {
|
||||
Ok(dir) => Some(dir),
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "Failed to get latest directory path");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
for entry in fs::read_dir(src)? {
|
||||
let entry = entry?;
|
||||
let src_path = entry.path();
|
||||
let dst_path = dst.join(entry.file_name());
|
||||
|
||||
if src_path == *backup_dir {
|
||||
tracing::debug!(
|
||||
path = %src_path.display(),
|
||||
"Skipping backup directory to avoid recursion"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(ref latest_dir) = latest_dir {
|
||||
if src_path == *latest_dir {
|
||||
tracing::debug!(
|
||||
path = %src_path.display(),
|
||||
"Skipping latest directory to avoid backing up current working data"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if src_path.is_dir() {
|
||||
copy_directory_recursive(&src_path, &dst_path)?;
|
||||
} else {
|
||||
fs::copy(&src_path, &dst_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn copy_directory_recursive(src: &PathBuf, dst: &PathBuf) -> Result<(), HoppError> {
|
||||
if !src.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fs::create_dir_all(dst)?;
|
||||
|
||||
for entry in fs::read_dir(src)? {
|
||||
let entry = entry?;
|
||||
let src_path = entry.path();
|
||||
let dst_path = dst.join(entry.file_name());
|
||||
|
||||
if src_path.is_dir() {
|
||||
copy_directory_recursive(&src_path, &dst_path)?;
|
||||
} else {
|
||||
fs::copy(&src_path, &dst_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_version_from_backup_dirname(dirname: &str) -> Option<Version> {
|
||||
// Parse "backup-by-v1.2.3" format
|
||||
if let Some(version_part) = dirname.strip_prefix("backup-by-v") {
|
||||
Version::parse(version_part).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_old_backups() -> Result<(), HoppError> {
|
||||
let backup_dir = path::backup_dir()?;
|
||||
|
||||
if !backup_dir.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let entries = fs::read_dir(&backup_dir)?;
|
||||
let mut version_paths = Vec::new();
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
if let Some(dirname) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if let Some(version) = parse_version_from_backup_dirname(dirname) {
|
||||
version_paths.push((version, path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if version_paths.len() <= MAX_BACKUP_COUNT {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
version_paths.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
let to_remove_count = version_paths.len() - MAX_BACKUP_COUNT;
|
||||
let to_remove = &version_paths[..to_remove_count];
|
||||
|
||||
for (version, path) in to_remove {
|
||||
tracing::info!(
|
||||
version = %version,
|
||||
path = %path.display(),
|
||||
"Removing old backup"
|
||||
);
|
||||
|
||||
match fs::remove_dir_all(path) {
|
||||
Ok(_) => {
|
||||
tracing::debug!(
|
||||
version = %version,
|
||||
"Successfully removed old backup"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
version = %version,
|
||||
error = %e,
|
||||
"Failed to remove old backup, continuing"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_parse_version_from_backup_dirname() {
|
||||
// Valid cases
|
||||
assert_eq!(
|
||||
parse_version_from_backup_dirname("backup-by-v1.2.3"),
|
||||
Some(Version::new(1, 2, 3))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_version_from_backup_dirname("backup-by-v10.0.0"),
|
||||
Some(Version::new(10, 0, 0))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_version_from_backup_dirname("backup-by-v2.1.0-beta.1"),
|
||||
Version::parse("2.1.0-beta.1").ok()
|
||||
);
|
||||
|
||||
// Invalid cases
|
||||
assert_eq!(parse_version_from_backup_dirname("backup-v1.2.3"), None);
|
||||
assert_eq!(parse_version_from_backup_dirname("v1.2.3"), None);
|
||||
assert_eq!(parse_version_from_backup_dirname("backup-by-v"), None);
|
||||
assert_eq!(
|
||||
parse_version_from_backup_dirname("backup-by-vinvalid"),
|
||||
None
|
||||
);
|
||||
assert_eq!(parse_version_from_backup_dirname(""), None);
|
||||
assert_eq!(parse_version_from_backup_dirname("random-dir"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_max_backup_count_constant() {
|
||||
assert_eq!(MAX_BACKUP_COUNT, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copy_directory_recursive_empty_dir() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let src = temp_dir.path().join("src");
|
||||
let dst = temp_dir.path().join("dst");
|
||||
|
||||
fs::create_dir_all(&src).unwrap();
|
||||
|
||||
let result = copy_directory_recursive(&src, &dst);
|
||||
assert!(result.is_ok());
|
||||
assert!(dst.exists());
|
||||
assert!(dst.is_dir());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copy_directory_recursive_with_files() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let src = temp_dir.path().join("src");
|
||||
let dst = temp_dir.path().join("dst");
|
||||
|
||||
fs::create_dir_all(&src).unwrap();
|
||||
fs::write(src.join("test.txt"), "test content").unwrap();
|
||||
fs::create_dir_all(src.join("subdir")).unwrap();
|
||||
fs::write(src.join("subdir").join("nested.txt"), "nested content").unwrap();
|
||||
|
||||
let result = copy_directory_recursive(&src, &dst);
|
||||
assert!(result.is_ok());
|
||||
|
||||
assert!(dst.exists());
|
||||
assert!(dst.join("test.txt").exists());
|
||||
assert!(dst.join("subdir").exists());
|
||||
assert!(dst.join("subdir").join("nested.txt").exists());
|
||||
|
||||
let content = fs::read_to_string(dst.join("test.txt")).unwrap();
|
||||
assert_eq!(content, "test content");
|
||||
|
||||
let nested_content = fs::read_to_string(dst.join("subdir").join("nested.txt")).unwrap();
|
||||
assert_eq!(nested_content, "nested content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copy_directory_recursive_nonexistent_src() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let src = temp_dir.path().join("nonexistent");
|
||||
let dst = temp_dir.path().join("dst");
|
||||
|
||||
let result = copy_directory_recursive(&src, &dst);
|
||||
assert!(result.is_ok()); // Should return Ok for nonexistent source
|
||||
assert!(!dst.exists()); // Destination should not be created
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version_sorting_in_cleanup() {
|
||||
let versions = vec![
|
||||
Version::new(1, 0, 0),
|
||||
Version::new(2, 1, 0),
|
||||
Version::new(1, 5, 0),
|
||||
Version::new(2, 0, 0),
|
||||
];
|
||||
|
||||
let mut version_paths: Vec<(Version, PathBuf)> = versions
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
(
|
||||
v.clone(),
|
||||
PathBuf::from(format!("backup-by-v{}", v.clone())),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
version_paths.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
// Should be sorted: 1.0.0, 1.5.0, 2.0.0, 2.1.0
|
||||
assert_eq!(version_paths[0].0, Version::new(1, 0, 0));
|
||||
assert_eq!(version_paths[1].0, Version::new(1, 5, 0));
|
||||
assert_eq!(version_paths[2].0, Version::new(2, 0, 0));
|
||||
assert_eq!(version_paths[3].0, Version::new(2, 1, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cleanup_old_backups_integration() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let backup_root = temp_dir.path();
|
||||
|
||||
let backup_dirs = vec![
|
||||
"backup-by-v1.0.0",
|
||||
"backup-by-v1.1.0",
|
||||
"backup-by-v1.2.0",
|
||||
"backup-by-v2.0.0",
|
||||
"backup-by-v2.1.0", // This should be kept (newest 3)
|
||||
"backup-by-v2.2.0", // This should be kept
|
||||
"backup-by-v3.0.0", // This should be kept
|
||||
"not-a-backup-dir", // This should be ignored
|
||||
];
|
||||
|
||||
for dir in &backup_dirs {
|
||||
fs::create_dir_all(backup_root.join(dir)).unwrap();
|
||||
}
|
||||
|
||||
let entries = fs::read_dir(backup_root).unwrap();
|
||||
let mut version_paths = Vec::new();
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
if let Some(dirname) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if let Some(version) = parse_version_from_backup_dirname(dirname) {
|
||||
version_paths.push((version, path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
version_paths.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
// Should have 7 valid backup directories (excluding "not-a-backup-dir")
|
||||
assert_eq!(version_paths.len(), 7);
|
||||
|
||||
// If MAX_BACKUP_COUNT is 3, we should remove 4 directories
|
||||
let should_remove = version_paths.len() > MAX_BACKUP_COUNT;
|
||||
assert!(should_remove);
|
||||
|
||||
if should_remove {
|
||||
let to_remove_count = version_paths.len() - MAX_BACKUP_COUNT;
|
||||
assert_eq!(to_remove_count, 4);
|
||||
|
||||
// The oldest versions should be marked for removal
|
||||
assert_eq!(version_paths[0].0, Version::new(1, 0, 0));
|
||||
assert_eq!(version_paths[1].0, Version::new(1, 1, 0));
|
||||
assert_eq!(version_paths[2].0, Version::new(1, 2, 0));
|
||||
assert_eq!(version_paths[3].0, Version::new(2, 0, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
117
packages/hoppscotch-desktop/src-tauri/src/config.rs
Normal file
117
packages/hoppscotch-desktop/src-tauri/src/config.rs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
use std::{fs, path::PathBuf, time::Duration};
|
||||
|
||||
use tauri_plugin_appload::{ApiConfig, CacheConfig, Config, StorageConfig, VendorConfig};
|
||||
|
||||
use crate::{error::HoppError, path};
|
||||
|
||||
const API_SERVER_URL: &str = "http://localhost:3200";
|
||||
const API_TIMEOUT_SECS: u64 = 30;
|
||||
const CACHE_MAX_SIZE_MB: usize = 1000;
|
||||
const CACHE_FILE_TTL_SECS: u64 = 3600;
|
||||
const CACHE_HOT_RATIO: f32 = 0.9;
|
||||
const MAX_BUNDLE_SIZE_MB: usize = 50;
|
||||
|
||||
pub struct HoppApploadConfig {
|
||||
bundle_path: PathBuf,
|
||||
manifest_path: PathBuf,
|
||||
config_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl HoppApploadConfig {
|
||||
pub fn new() -> Result<Self, HoppError> {
|
||||
let config_dir = path::config_dir().unwrap_or_else(|e| {
|
||||
tracing::error!(error = %e, "Failed to create config directory, using temp dir");
|
||||
std::env::temp_dir().join(path::APP_ID)
|
||||
});
|
||||
|
||||
let bundle_path = path::bundle_path();
|
||||
let manifest_path = path::manifest_path();
|
||||
|
||||
Ok(Self {
|
||||
bundle_path,
|
||||
manifest_path,
|
||||
config_dir,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write_vendored(&self) -> Result<(), HoppError> {
|
||||
fs::write(&self.bundle_path, include_bytes!("../../bundle.zip"))?;
|
||||
fs::write(&self.manifest_path, include_bytes!("../../manifest.json"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn build(&self) -> Config {
|
||||
Config::builder()
|
||||
.api(ApiConfig {
|
||||
server_url: API_SERVER_URL.to_string(),
|
||||
timeout: Duration::from_secs(API_TIMEOUT_SECS),
|
||||
})
|
||||
.cache(CacheConfig {
|
||||
max_size: CACHE_MAX_SIZE_MB * 1024 * 1024,
|
||||
file_ttl: Duration::from_secs(CACHE_FILE_TTL_SECS),
|
||||
hot_ratio: CACHE_HOT_RATIO,
|
||||
})
|
||||
.storage(StorageConfig {
|
||||
root_dir: self.config_dir.clone(),
|
||||
max_bundle_size: MAX_BUNDLE_SIZE_MB * 1024 * 1024,
|
||||
})
|
||||
.vendor(VendorConfig {
|
||||
bundle_path: self.bundle_path.clone(),
|
||||
manifest_path: self.manifest_path.clone(),
|
||||
})
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_constants() {
|
||||
// NOTE: These are rather pointless tests, but are here just in case
|
||||
// there are rebase/merge conflicts that rewrites the values
|
||||
// (since there's been quite a lot of experimentation on that front)
|
||||
// so this created on a new branch shall remain consistent.
|
||||
assert_eq!(API_SERVER_URL, "http://localhost:3200");
|
||||
assert_eq!(API_TIMEOUT_SECS, 30);
|
||||
assert_eq!(CACHE_MAX_SIZE_MB, 1000);
|
||||
assert_eq!(CACHE_FILE_TTL_SECS, 3600);
|
||||
assert_eq!(CACHE_HOT_RATIO, 0.9);
|
||||
assert_eq!(MAX_BUNDLE_SIZE_MB, 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hopp_appload_config_new() {
|
||||
let config = HoppApploadConfig::new();
|
||||
assert!(config.is_ok());
|
||||
|
||||
let config = config.unwrap();
|
||||
assert!(config
|
||||
.bundle_path
|
||||
.to_string_lossy()
|
||||
.contains("hopp_bundle.zip"));
|
||||
assert!(config
|
||||
.manifest_path
|
||||
.to_string_lossy()
|
||||
.contains("hopp_manifest.json"));
|
||||
assert!(!config.config_dir.as_os_str().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_paths() {
|
||||
let config = HoppApploadConfig::new().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config.bundle_path.file_name().unwrap().to_str().unwrap(),
|
||||
"hopp_bundle.zip"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.manifest_path.file_name().unwrap().to_str().unwrap(),
|
||||
"hopp_manifest.json"
|
||||
);
|
||||
|
||||
assert!(!config.config_dir.as_os_str().is_empty());
|
||||
}
|
||||
}
|
||||
58
packages/hoppscotch-desktop/src-tauri/src/dialog.rs
Normal file
58
packages/hoppscotch-desktop/src-tauri/src/dialog.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
use native_dialog::{MessageDialog, MessageType};
|
||||
|
||||
pub fn panic(msg: &str) {
|
||||
const FATAL_ERROR: &str = "Fatal error";
|
||||
|
||||
MessageDialog::new()
|
||||
.set_type(MessageType::Error)
|
||||
.set_title(FATAL_ERROR)
|
||||
.set_text(msg)
|
||||
.show_alert()
|
||||
.unwrap_or_default();
|
||||
|
||||
tracing::error!("{}: {}", FATAL_ERROR, msg);
|
||||
|
||||
panic!("{}: {}", FATAL_ERROR, msg);
|
||||
}
|
||||
|
||||
pub fn info(msg: &str) {
|
||||
tracing::info!("{}", msg);
|
||||
|
||||
MessageDialog::new()
|
||||
.set_type(MessageType::Info)
|
||||
.set_title("Info")
|
||||
.set_text(msg)
|
||||
.show_alert()
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
pub fn warn(msg: &str) {
|
||||
tracing::warn!("{}", msg);
|
||||
|
||||
MessageDialog::new()
|
||||
.set_type(MessageType::Warning)
|
||||
.set_title("Warning")
|
||||
.set_text(msg)
|
||||
.show_alert()
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
pub fn error(msg: &str) {
|
||||
tracing::error!("{}", msg);
|
||||
|
||||
MessageDialog::new()
|
||||
.set_type(MessageType::Error)
|
||||
.set_title("Error")
|
||||
.set_text(msg)
|
||||
.show_alert()
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
pub fn confirm(title: &str, msg: &str, icon: MessageType) -> bool {
|
||||
MessageDialog::new()
|
||||
.set_type(icon)
|
||||
.set_title(title)
|
||||
.set_text(msg)
|
||||
.show_confirm()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
18
packages/hoppscotch-desktop/src-tauri/src/error.rs
Normal file
18
packages/hoppscotch-desktop/src-tauri/src/error.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
use std::io;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum HoppError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
|
||||
#[error("Failed to initialize server port")]
|
||||
ServerPortInitialization,
|
||||
|
||||
#[error("Failed to emit event: {0}")]
|
||||
EventEmission(String),
|
||||
|
||||
#[error("Tauri error: {0}")]
|
||||
Tauri(#[from] tauri::Error),
|
||||
}
|
||||
|
|
@ -1,22 +1,116 @@
|
|||
pub mod backup;
|
||||
pub mod config;
|
||||
pub mod dialog;
|
||||
pub mod error;
|
||||
pub mod logger;
|
||||
pub mod path;
|
||||
pub mod server;
|
||||
pub mod updater;
|
||||
pub mod util;
|
||||
pub mod webview;
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use tauri::{Emitter, Manager};
|
||||
use tauri_plugin_appload::VendorConfigBuilder;
|
||||
use tauri::Emitter;
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use tauri_plugin_window_state::StateFlags;
|
||||
|
||||
use random_port::{PortPicker, Protocol};
|
||||
|
||||
pub const HOPPSCOTCH_DESKTOP_IDENTIFIER: &'static str = "io.hoppscotch.desktop";
|
||||
use config::HoppApploadConfig;
|
||||
use error::HoppError;
|
||||
|
||||
static SERVER_PORT: OnceLock<u16> = OnceLock::new();
|
||||
|
||||
#[tauri::command]
|
||||
fn is_portable() -> bool {
|
||||
cfg!(feature = "portable")
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn hopp_auth_port() -> u16 {
|
||||
*SERVER_PORT.get().expect("Server port not initialized")
|
||||
SERVER_PORT
|
||||
.get()
|
||||
.copied()
|
||||
.expect("Server port not initialized")
|
||||
}
|
||||
|
||||
fn setup_deep_link_handler(app: &tauri::App) -> Result<(), HoppError> {
|
||||
let handle = app.handle().clone();
|
||||
|
||||
app.deep_link().on_open_url(move |event| {
|
||||
let urls = event.urls();
|
||||
tracing::info!(
|
||||
urls = ?urls,
|
||||
count = urls.len(),
|
||||
"Processing deep link request"
|
||||
);
|
||||
|
||||
if let Err(e) = handle.emit("scheme-request-received", urls) {
|
||||
tracing::error!(
|
||||
error.message = %e,
|
||||
error.type = %std::any::type_name_of_val(&e),
|
||||
"Deep link event emission failed"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
tracing::info!(app_name = %app.package_info().name, "Configured deep link handler");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_server(app: &tauri::App) -> Result<u16, HoppError> {
|
||||
let server_port: u16 = PortPicker::new()
|
||||
.protocol(Protocol::Tcp)
|
||||
.port_range(15000..=25000)
|
||||
.pick()
|
||||
.map_err(|_| HoppError::ServerPortInitialization)?;
|
||||
|
||||
tracing::info!("Selected server port: {}", server_port);
|
||||
SERVER_PORT
|
||||
.set(server_port)
|
||||
.map_err(|_| HoppError::ServerPortInitialization)?;
|
||||
|
||||
let port = *SERVER_PORT
|
||||
.get()
|
||||
.ok_or(HoppError::ServerPortInitialization)?;
|
||||
tracing::info!(port = port, "Initializing server with pre-selected port");
|
||||
|
||||
let port = server::init(port, app.handle().clone());
|
||||
tracing::info!(port = port, "Server initialization complete");
|
||||
|
||||
Ok(port)
|
||||
}
|
||||
|
||||
async fn setup_version_backup(app: &tauri::App) -> Result<(), HoppError> {
|
||||
tracing::info!("Checking for version changes and performing backup if needed");
|
||||
|
||||
let handle = app.handle().clone();
|
||||
match tauri::async_runtime::spawn_blocking(move || {
|
||||
backup::perform_version_check_and_backup(handle)
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(Ok(_)) => {
|
||||
tracing::info!("Version backup check completed successfully");
|
||||
Ok(())
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
tracing::error!(
|
||||
error = %e,
|
||||
"Version backup check failed, but continuing with startup"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
error = %e,
|
||||
"Version backup task panicked, but continuing with startup"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gracefully quit the Hoppscotch Desktop
|
||||
|
|
@ -40,45 +134,55 @@ fn quit_app(app: tauri::AppHandle) -> Result<(), String> {
|
|||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
let mode = if cfg!(feature = "portable") {
|
||||
"portable"
|
||||
} else {
|
||||
"standard"
|
||||
};
|
||||
tracing::info!(mode = mode, "Hoppscotch Desktop running in {} mode", mode);
|
||||
|
||||
#[cfg(all(feature = "portable", windows))]
|
||||
{
|
||||
tracing::debug!("Checking WebView initialization for portable Windows variant");
|
||||
webview::init_webview();
|
||||
}
|
||||
|
||||
let appload_config = match HoppApploadConfig::new() {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Failed to initialize application configuration");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = appload_config.write_vendored() {
|
||||
tracing::error!(error = %e, "Failed to write bundled files");
|
||||
return;
|
||||
}
|
||||
|
||||
let appload_config = appload_config.build();
|
||||
|
||||
let app = tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
let server_port: u16 = PortPicker::new()
|
||||
.protocol(Protocol::Tcp)
|
||||
.port_range(15000..=25000)
|
||||
.pick()
|
||||
.expect("Cannot find unused port");
|
||||
tracing::info!("Selected server port: {}", server_port);
|
||||
SERVER_PORT
|
||||
.set(server_port)
|
||||
.expect("Failed to set server port");
|
||||
let port = *SERVER_PORT.get().expect("Server port not initialized");
|
||||
tracing::info!(port = port, "Initializing server with pre-selected port");
|
||||
let port = server::init(port, app.handle().clone());
|
||||
tracing::info!(port = port, "Server initialization complete");
|
||||
tauri::async_runtime::block_on(async {
|
||||
if let Err(e) = setup_version_backup(app).await {
|
||||
tracing::error!(error = %e, "Failed to setup version backup");
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
tracing::info!(app_name = %app.package_info().name, "Configuring deep link handler");
|
||||
let handle = app.handle().clone();
|
||||
app.deep_link().on_open_url(move |event| {
|
||||
let urls = event.urls();
|
||||
tracing::info!(
|
||||
urls = ?urls,
|
||||
count = urls.len(),
|
||||
"Processing deep link request"
|
||||
);
|
||||
handle
|
||||
.emit("scheme-request-received", urls)
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::error!(
|
||||
error.message = %e,
|
||||
error.type = %std::any::type_name_of_val(&e),
|
||||
"Deep link event emission failed"
|
||||
);
|
||||
});
|
||||
});
|
||||
if let Err(e) = setup_server(app) {
|
||||
tracing::error!(error = %e, "Failed to setup server");
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
tracing::info!("Starting Hoppscotch Desktop v{}", env!("CARGO_PKG_VERSION"));
|
||||
if let Err(e) = setup_deep_link_handler(app) {
|
||||
tracing::error!(error = %e, "Failed to setup deep link handler");
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
tracing::info!("Starting Hoppscotch Desktop v{}", env!("CARGO_PKG_VERSION"));
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.plugin(
|
||||
tauri_plugin_window_state::Builder::new()
|
||||
|
|
@ -92,25 +196,32 @@ pub fn run() {
|
|||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_appload::init(
|
||||
VendorConfigBuilder::new().bundle(
|
||||
include_bytes!("../../bundle.zip").to_vec(),
|
||||
include_bytes!("../../manifest.json"),
|
||||
),
|
||||
))
|
||||
.plugin(tauri_plugin_appload::init(appload_config))
|
||||
.plugin(tauri_plugin_relay::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
is_portable,
|
||||
hopp_auth_port,
|
||||
updater::check_updates_available,
|
||||
updater::install_updates_and_restart,
|
||||
quit_app,
|
||||
backup::check_and_backup_on_version_change,
|
||||
updater::check_for_updates,
|
||||
path::get_config_dir,
|
||||
path::get_latest_dir,
|
||||
path::get_instance_dir,
|
||||
path::get_store_dir,
|
||||
path::get_backup_dir,
|
||||
path::get_logs_dir,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
.run(tauri::generate_context!());
|
||||
|
||||
if let Err(e) = app {
|
||||
tracing::error!(error = %e, "Error while running Hoppscotch Desktop");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::path::PathBuf;
|
|||
use file_rotate::{compression::Compression, suffix::AppendCount, ContentLimit, FileRotate};
|
||||
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||
|
||||
use crate::HOPPSCOTCH_DESKTOP_IDENTIFIER;
|
||||
use crate::path;
|
||||
|
||||
pub struct LogGuard(pub tracing_appender::non_blocking::WorkerGuard);
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ pub fn setup(log_dir: &PathBuf) -> Result<LogGuard, Box<dyn std::error::Error>>
|
|||
|
||||
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| "debug".into());
|
||||
|
||||
let log_file_path = log_dir.join(&format!("{}.log", HOPPSCOTCH_DESKTOP_IDENTIFIER));
|
||||
let log_file_path = path::log_file_path();
|
||||
tracing::info!(log_file_path =? &log_file_path);
|
||||
|
||||
let file = FileRotate::new(
|
||||
|
|
|
|||
|
|
@ -3,37 +3,31 @@
|
|||
|
||||
use hoppscotch_desktop_lib::{
|
||||
logger::{self, LogGuard},
|
||||
HOPPSCOTCH_DESKTOP_IDENTIFIER,
|
||||
path,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
// Follows how `tauri` does this
|
||||
// see: https://github.com/tauri-apps/tauri/blob/dev/crates/tauri/src/path/desktop.rs
|
||||
let path = {
|
||||
#[cfg(target_os = "macos")]
|
||||
let path = dirs::home_dir()
|
||||
.map(|dir| dir.join("Library/Logs").join(HOPPSCOTCH_DESKTOP_IDENTIFIER));
|
||||
#[cfg(feature = "portable")]
|
||||
println!("Starting Hoppscotch Desktop in PORTABLE mode");
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let path =
|
||||
dirs::data_local_dir().map(|dir| dir.join(HOPPSCOTCH_DESKTOP_IDENTIFIER).join("logs"));
|
||||
#[cfg(not(feature = "portable"))]
|
||||
println!("Starting Hoppscotch Desktop in STANDARD mode");
|
||||
|
||||
path
|
||||
let log_dir = match path::logs_dir() {
|
||||
Ok(dir) => {
|
||||
println!("Log directory: {}", dir.display());
|
||||
dir
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to setup logging directory: {}", e);
|
||||
println!("Starting Hoppscotch Desktop without logging...");
|
||||
return hoppscotch_desktop_lib::run();
|
||||
}
|
||||
};
|
||||
|
||||
let Some(log_file_path) = path else {
|
||||
eprint!("Failed to setup logging!");
|
||||
|
||||
println!("Starting Hoppscotch Desktop...");
|
||||
|
||||
return hoppscotch_desktop_lib::run();
|
||||
};
|
||||
|
||||
let Ok(LogGuard(guard)) = logger::setup(&log_file_path) else {
|
||||
eprint!("Failed to setup logging!");
|
||||
|
||||
println!("Starting Hoppscotch Desktop...");
|
||||
|
||||
let Ok(LogGuard(guard)) = logger::setup(&log_dir) else {
|
||||
eprintln!("Failed to setup logging!");
|
||||
println!("Starting Hoppscotch Desktop without logging...");
|
||||
return hoppscotch_desktop_lib::run();
|
||||
};
|
||||
|
||||
|
|
@ -42,7 +36,30 @@ fn main() {
|
|||
// so safe to have it like this.
|
||||
let _guard = guard;
|
||||
|
||||
tracing::info!("Starting Hoppscotch Desktop...");
|
||||
#[cfg(feature = "portable")]
|
||||
{
|
||||
tracing::info!(
|
||||
"Hoppscotch Desktop v{} starting in PORTABLE mode",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
if let Ok(config_dir) = path::config_dir() {
|
||||
tracing::info!("Config directory (portable): {}", config_dir.display());
|
||||
}
|
||||
if let Ok(current_dir) = std::env::current_dir() {
|
||||
tracing::info!("Current working directory: {}", current_dir.display());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "portable"))]
|
||||
{
|
||||
tracing::info!(
|
||||
"Hoppscotch Desktop v{} starting in STANDARD mode",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
if let Ok(config_dir) = path::config_dir() {
|
||||
tracing::info!("Config directory (standard): {}", config_dir.display());
|
||||
}
|
||||
}
|
||||
|
||||
hoppscotch_desktop_lib::run()
|
||||
}
|
||||
|
|
|
|||
314
packages/hoppscotch-desktop/src-tauri/src/path.rs
Normal file
314
packages/hoppscotch-desktop/src-tauri/src/path.rs
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// App identifier (identical to `tauri.conf.json`)
|
||||
/// used for various directories and configurations
|
||||
pub const APP_ID: &str = "io.hoppscotch.desktop";
|
||||
|
||||
pub fn config_dir() -> io::Result<PathBuf> {
|
||||
let path = platform_config_dir();
|
||||
std::fs::create_dir_all(&path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub fn latest_dir() -> io::Result<PathBuf> {
|
||||
let path = config_dir()?.join("latest");
|
||||
std::fs::create_dir_all(&path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub fn instance_dir() -> io::Result<PathBuf> {
|
||||
let path = latest_dir()?.join("instance");
|
||||
std::fs::create_dir_all(&path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub fn store_dir() -> io::Result<PathBuf> {
|
||||
let path = latest_dir()?.join("store");
|
||||
std::fs::create_dir_all(&path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub fn get_versioned_backup_dir(version: &str) -> io::Result<PathBuf> {
|
||||
let backup_root = backup_dir()?;
|
||||
let versioned_path = backup_root.join(format!("v{}", version));
|
||||
std::fs::create_dir_all(&versioned_path)?;
|
||||
Ok(versioned_path)
|
||||
}
|
||||
|
||||
pub fn backup_dir() -> io::Result<PathBuf> {
|
||||
let path = config_dir()?.join("backup");
|
||||
std::fs::create_dir_all(&path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
pub fn logs_dir() -> io::Result<PathBuf> {
|
||||
let path = platform_logs_dir();
|
||||
std::fs::create_dir_all(&path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_config_dir() -> Result<String, String> {
|
||||
config_dir()
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_latest_dir() -> Result<String, String> {
|
||||
latest_dir()
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_instance_dir() -> Result<String, String> {
|
||||
instance_dir()
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_store_dir() -> Result<String, String> {
|
||||
store_dir()
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_backup_dir() -> Result<String, String> {
|
||||
backup_dir()
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_logs_dir() -> Result<String, String> {
|
||||
logs_dir()
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
pub fn log_file_path() -> PathBuf {
|
||||
platform_logs_dir().join(format!("{}.log", APP_ID))
|
||||
}
|
||||
|
||||
pub fn bundle_path() -> PathBuf {
|
||||
if cfg!(feature = "portable") {
|
||||
std::env::current_dir()
|
||||
.unwrap_or_else(|_| std::env::temp_dir())
|
||||
.join("hopp_bundle.zip")
|
||||
} else {
|
||||
std::env::temp_dir().join("hopp_bundle.zip")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn manifest_path() -> PathBuf {
|
||||
if cfg!(feature = "portable") {
|
||||
std::env::current_dir()
|
||||
.unwrap_or_else(|_| std::env::temp_dir())
|
||||
.join("hopp_manifest.json")
|
||||
} else {
|
||||
std::env::temp_dir().join("hopp_manifest.json")
|
||||
}
|
||||
}
|
||||
|
||||
// Follows how `tauri` does this
|
||||
// see: https://github.com/tauri-apps/tauri/blob/dev/crates/tauri/src/path/desktop.rs
|
||||
fn platform_config_dir() -> PathBuf {
|
||||
if cfg!(feature = "portable") {
|
||||
return std::env::current_dir()
|
||||
.unwrap_or_else(|_| std::env::temp_dir())
|
||||
.join("hoppscotch-desktop-data");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
dirs::home_dir()
|
||||
.map(|dir| dir.join("Library/Application Support").join(APP_ID))
|
||||
.unwrap_or_else(|| std::env::temp_dir().join(APP_ID))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
dirs::config_dir()
|
||||
.map(|dir| dir.join(APP_ID))
|
||||
.unwrap_or_else(|| std::env::temp_dir().join(APP_ID))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
dirs::config_dir()
|
||||
.map(|dir| dir.join(APP_ID))
|
||||
.unwrap_or_else(|| std::env::temp_dir().join(APP_ID))
|
||||
}
|
||||
|
||||
// Fallback for others
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
||||
{
|
||||
std::env::temp_dir().join(APP_ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Follows how `tauri` does this
|
||||
// see: https://github.com/tauri-apps/tauri/blob/dev/crates/tauri/src/path/desktop.rs
|
||||
fn platform_logs_dir() -> PathBuf {
|
||||
if cfg!(feature = "portable") {
|
||||
return std::env::current_dir()
|
||||
.unwrap_or_else(|_| std::env::temp_dir())
|
||||
.join("logs");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
dirs::home_dir()
|
||||
.map(|dir| dir.join("Library/Logs").join(APP_ID))
|
||||
.unwrap_or_else(|| std::env::temp_dir().join(APP_ID).join("logs"))
|
||||
}
|
||||
|
||||
// Also fallback for others
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
dirs::data_local_dir()
|
||||
.map(|dir| dir.join(APP_ID).join("logs"))
|
||||
.unwrap_or_else(|| std::env::temp_dir().join(APP_ID).join("logs"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_app_id_constant() {
|
||||
assert_eq!(APP_ID, "io.hoppscotch.desktop");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_file_path() {
|
||||
let path = log_file_path();
|
||||
assert!(path.to_string_lossy().contains("io.hoppscotch.desktop.log"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manifest_path_portable() {
|
||||
let path = manifest_path();
|
||||
assert!(path.to_string_lossy().ends_with("hopp_manifest.json"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_platform_config_dir_structure() {
|
||||
let config_dir = platform_config_dir();
|
||||
|
||||
#[cfg(feature = "portable")]
|
||||
{
|
||||
assert!(config_dir
|
||||
.to_string_lossy()
|
||||
.contains("hoppscotch-desktop-data"));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "portable"))]
|
||||
{
|
||||
assert!(config_dir.to_string_lossy().contains(APP_ID));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_platform_logs_dir_structure() {
|
||||
let logs_dir = platform_logs_dir();
|
||||
|
||||
#[cfg(feature = "portable")]
|
||||
{
|
||||
assert!(logs_dir.to_string_lossy().ends_with("logs"));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "portable"))]
|
||||
{
|
||||
assert!(logs_dir.to_string_lossy().contains(APP_ID));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_config_dir_command() {
|
||||
let result = get_config_dir();
|
||||
assert!(result.is_ok());
|
||||
let path_str = result.unwrap();
|
||||
assert!(!path_str.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_logs_dir_command() {
|
||||
let result = get_logs_dir();
|
||||
assert!(result.is_ok());
|
||||
let path_str = result.unwrap();
|
||||
assert!(!path_str.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod portable_tests {
|
||||
// NOTE: These tests should only run when
|
||||
// the portable feature flag is enabled
|
||||
#[cfg(feature = "portable")]
|
||||
#[test]
|
||||
fn test_portable_bundle_path() {
|
||||
let path = bundle_path();
|
||||
assert!(path.file_name().unwrap() == "hopp_bundle.zip");
|
||||
}
|
||||
|
||||
#[cfg(feature = "portable")]
|
||||
#[test]
|
||||
fn test_portable_manifest_path() {
|
||||
let path = manifest_path();
|
||||
assert!(path.file_name().unwrap() == "hopp_manifest.json");
|
||||
}
|
||||
|
||||
#[cfg(feature = "portable")]
|
||||
#[test]
|
||||
fn test_portable_config_dir_structure() {
|
||||
let config_dir = platform_config_dir();
|
||||
assert!(config_dir
|
||||
.to_string_lossy()
|
||||
.contains("hoppscotch-desktop-data"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod platform_tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn test_macos_paths() {
|
||||
let config_dir = platform_config_dir();
|
||||
if !cfg!(feature = "portable") {
|
||||
assert!(config_dir
|
||||
.to_string_lossy()
|
||||
.contains("Library/Application Support"));
|
||||
}
|
||||
|
||||
let logs_dir = platform_logs_dir();
|
||||
if !cfg!(feature = "portable") {
|
||||
assert!(logs_dir.to_string_lossy().contains("Library/Logs"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn test_windows_paths() {
|
||||
let config_dir = platform_config_dir();
|
||||
if !cfg!(feature = "portable") {
|
||||
assert!(config_dir.to_string_lossy().contains(APP_ID));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn test_linux_paths() {
|
||||
let config_dir = platform_config_dir();
|
||||
if !cfg!(feature = "portable") {
|
||||
assert!(config_dir.to_string_lossy().contains(APP_ID));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +1,13 @@
|
|||
use tauri_plugin_dialog::DialogExt;
|
||||
use crate::{dialog, util};
|
||||
use native_dialog::MessageType;
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
/// Check for updates using the updater and return whether updates are available
|
||||
/// This mimics the behavior of `checkForUpdates` in `updater.ts` but uses native dialogs when needed
|
||||
#[tauri::command]
|
||||
pub async fn check_updates_available(app: tauri::AppHandle) -> Result<bool, String> {
|
||||
tracing::info!("Checking for updates...");
|
||||
let updater = match app.updater() {
|
||||
Ok(updater) => updater,
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Failed to initialize updater");
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
pub async fn check_for_updates(app: tauri::AppHandle) -> Result<bool, String> {
|
||||
tracing::info!("Checking for portable updates");
|
||||
|
||||
match updater.check().await {
|
||||
Ok(Some(_update)) => {
|
||||
tracing::info!("Update available");
|
||||
Ok(true)
|
||||
}
|
||||
Ok(None) => {
|
||||
tracing::info!("No updates available");
|
||||
Ok(false)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Failed to check for updates");
|
||||
Err(format!("Failed to check for updates: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_updates_and_restart(app: tauri::AppHandle) -> Result<(), String> {
|
||||
tracing::info!("Installing updates...");
|
||||
let updater = match app.updater() {
|
||||
Ok(updater) => updater,
|
||||
Err(e) => {
|
||||
|
|
@ -44,48 +21,29 @@ pub async fn install_updates_and_restart(app: tauri::AppHandle) -> Result<(), St
|
|||
tracing::info!(
|
||||
current_version = app.package_info().version.to_string(),
|
||||
update_version = update.version.to_string(),
|
||||
"Installing update"
|
||||
"Update available"
|
||||
);
|
||||
let download_url = "https://hoppscotch.com/download";
|
||||
let message = format!(
|
||||
"An update (version {}) is available for Hoppscotch Desktop (Portable).\n\nWould you like to download it now?\n\n• Yes = Download now\n• No = Remind me later",
|
||||
update.version
|
||||
);
|
||||
|
||||
let dialog = app.dialog();
|
||||
let should_update = dialog
|
||||
.message(format!(
|
||||
"A new version of Hoppscotch (v{}) is available.\n\n{}",
|
||||
update.version,
|
||||
update.body.as_ref().unwrap_or(&"".to_string())
|
||||
))
|
||||
.title("Update Available")
|
||||
.kind(tauri_plugin_dialog::MessageDialogKind::Info)
|
||||
.buttons(tauri_plugin_dialog::MessageDialogButtons::YesNo)
|
||||
.blocking_show();
|
||||
|
||||
if should_update {
|
||||
tracing::info!("User agreed to update, starting download...");
|
||||
match update.download_and_install(|_, _| {}, || {}).await {
|
||||
Ok(_) => {
|
||||
tracing::info!("Update installed successfully, restarting app");
|
||||
app.restart();
|
||||
Err("Unreachable - app should have restarted".to_string())
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Failed to download or install update");
|
||||
let _ = app
|
||||
.dialog()
|
||||
.message(format!("Failed to install update: {}", e))
|
||||
.title("Update Error")
|
||||
.kind(tauri_plugin_dialog::MessageDialogKind::Error)
|
||||
.blocking_show();
|
||||
Err(format!("Failed to download or install update: {}", e))
|
||||
}
|
||||
if dialog::confirm("Download Update", &message, MessageType::Info) {
|
||||
if let None = util::open_link(download_url) {
|
||||
dialog::error(&format!(
|
||||
"Failed to open download page. Please visit {}",
|
||||
download_url
|
||||
));
|
||||
return Err(format!("Failed to open download URL"));
|
||||
}
|
||||
} else {
|
||||
tracing::info!("User declined the update");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
Ok(None) => {
|
||||
tracing::info!("No updates available");
|
||||
Ok(())
|
||||
Ok(false)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Failed to check for updates");
|
||||
|
|
|
|||
107
packages/hoppscotch-desktop/src-tauri/src/util.rs
Normal file
107
packages/hoppscotch-desktop/src-tauri/src/util.rs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
use std::process::{Command, Stdio};
|
||||
|
||||
pub fn open_link(link: &str) -> Option<()> {
|
||||
let null = Stdio::null();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Command::new("rundll32")
|
||||
.args(["url.dll,FileProtocolHandler", link])
|
||||
.stdout(null)
|
||||
.spawn()
|
||||
.ok()
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Command::new("open")
|
||||
.arg(link)
|
||||
.stdout(null)
|
||||
.spawn()
|
||||
.ok()
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Command::new("xdg-open")
|
||||
.arg(link)
|
||||
.stdout(null)
|
||||
.spawn()
|
||||
.ok()
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// NOTE: These tests won't actually open URLs in testing environments
|
||||
// but these will test the command construction logic
|
||||
#[test]
|
||||
fn test_open_link_with_valid_url() {
|
||||
let test_url = "https://example.com";
|
||||
|
||||
// The function should not panic and should return `Some(())` or `None`
|
||||
// depending on whether the command can be spawned
|
||||
let result = open_link(test_url);
|
||||
|
||||
// This should return `Some(())` if the command exists,
|
||||
// on unsupported platforms, this should return None
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||
{
|
||||
assert!(result.is_some() || result.is_none());
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_open_link_with_empty_string() {
|
||||
let result = open_link("");
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||
{
|
||||
assert!(result.is_some() || result.is_none());
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_open_link_with_special_characters() {
|
||||
let test_urls = vec![
|
||||
"https://example.com/path with spaces",
|
||||
"https://example.com/path?query=value&other=test",
|
||||
"https://example.com/path#fragment",
|
||||
"file:///path/to/local/file.txt",
|
||||
];
|
||||
|
||||
for url in test_urls {
|
||||
let result = open_link(url);
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||
{
|
||||
assert!(result.is_some() || result.is_none());
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
assert_eq!(result, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
packages/hoppscotch-desktop/src-tauri/src/webview/error.rs
Normal file
15
packages/hoppscotch-desktop/src-tauri/src/webview/error.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use std::io;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum WebViewError {
|
||||
#[error("Failed to open URL: {0}")]
|
||||
UrlOpen(#[from] io::Error),
|
||||
#[error("Failed to download WebView2 installer: {0}")]
|
||||
Download(String),
|
||||
#[error("WebView2 installation failed: {0}")]
|
||||
Installation(String),
|
||||
#[error("Failed during request: {0}")]
|
||||
Request(#[from] tauri_plugin_http::reqwest::Error),
|
||||
}
|
||||
212
packages/hoppscotch-desktop/src-tauri/src/webview/mod.rs
Normal file
212
packages/hoppscotch-desktop/src-tauri/src/webview/mod.rs
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
/// The WebView2 Runtime is a critical dependency for Tauri applications on Windows.
|
||||
/// We need to check for its presence, see [Source: GitHub Issue #59 - Portable windows build](https://github.com/tauri-apps/tauri-action/issues/59#issuecomment-827142638)
|
||||
///
|
||||
/// > "Tauri requires an installer if you define app resources, external binaries or running on environments that do not have Webview2 runtime installed. So I don't think it's a good idea to have a "portable" option since a Tauri binary itself isn't 100% portable."
|
||||
///
|
||||
/// The approach for checking WebView2 installation is based on Microsoft's official documentation, which states:
|
||||
///
|
||||
/// > ###### Detect if a WebView2 Runtime is already installed
|
||||
/// >
|
||||
/// > To verify that a WebView2 Runtime is installed, use one of the following approaches:
|
||||
/// >
|
||||
/// > * Approach 1: Inspect the `pv (REG_SZ)` regkey for the WebView2 Runtime at both of the following registry locations. The `HKEY_LOCAL_MACHINE` regkey is used for _per-machine_ install. The `HKEY_CURRENT_USER` regkey is used for _per-user_ install.
|
||||
/// >
|
||||
/// > For WebView2 applications, at least one of these regkeys must be present and defined with a version greater than 0.0.0.0. If neither regkey exists, or if only one of these regkeys exists but its value is `null`, an empty string, or 0.0.0.0, this means that the WebView2 Runtime isn't installed on the client. Inspect these regkeys to detect whether the WebView2 Runtime is installed, and to get the version of the WebView2 Runtime. Find `pv (REG_SZ)` at the following two locations.
|
||||
/// >
|
||||
/// > The two registry locations to inspect on 64-bit Windows:
|
||||
/// >
|
||||
/// > ```text
|
||||
/// > HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}
|
||||
/// >
|
||||
/// > HKEY_CURRENT_USER\Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}
|
||||
/// > ```
|
||||
/// >
|
||||
/// > The two registry locations to inspect on 32-bit Windows:
|
||||
/// >
|
||||
/// > ```text
|
||||
/// > HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}
|
||||
/// >
|
||||
/// > HKEY_CURRENT_USER\Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}
|
||||
/// > ```
|
||||
/// >
|
||||
/// > * Approach 2: Run [GetAvailableCoreWebView2BrowserVersionString](/microsoft-edge/webview2/reference/win32/webview2-idl#getavailablecorewebview2browserversionstring) and evaluate whether the `versionInfo` is `nullptr`. `nullptr` indicates that the WebView2 Runtime isn't installed. This API returns version information for the WebView2 Runtime or for any installed preview channels of Microsoft Edge (Beta, Dev, or Canary).
|
||||
///
|
||||
/// See: https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution?tabs=dotnetcsharp#detect-if-a-webview2-runtime-is-already-installed
|
||||
///
|
||||
/// Our implementation uses Approach 1, checking both the 32-bit (WOW6432Node) and 64-bit registry locations
|
||||
/// to make sure we have critical dependency compatibility with different system architectures.
|
||||
pub mod error;
|
||||
|
||||
use std::{io, ops::Not};
|
||||
|
||||
use native_dialog::MessageType;
|
||||
|
||||
use crate::{dialog, util};
|
||||
use error::WebViewError;
|
||||
|
||||
#[cfg(windows)]
|
||||
use {
|
||||
std::io::Cursor,
|
||||
std::process::Command,
|
||||
tauri_plugin_http::reqwest,
|
||||
tempfile::TempDir,
|
||||
winreg::{
|
||||
enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE},
|
||||
RegKey,
|
||||
},
|
||||
};
|
||||
|
||||
const TAURI_WEBVIEW_REF: &str = "https://v2.tauri.app/references/webview-versions/";
|
||||
const WINDOWS_WEBVIEW_REF: &str =
|
||||
"https://developer.microsoft.com/microsoft-edge/webview2/#download-section";
|
||||
|
||||
fn is_available() -> bool {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
const KEY_WOW64: &str = r"SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}";
|
||||
const KEY: &str =
|
||||
r"SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}";
|
||||
|
||||
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
|
||||
[
|
||||
hklm.open_subkey(KEY_WOW64),
|
||||
hkcu.open_subkey(KEY_WOW64),
|
||||
hklm.open_subkey(KEY),
|
||||
hkcu.open_subkey(KEY),
|
||||
]
|
||||
.into_iter()
|
||||
.any(|result| result.is_ok())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn open_install_website() -> Result<(), WebViewError> {
|
||||
let url = if cfg!(windows) {
|
||||
WINDOWS_WEBVIEW_REF
|
||||
} else {
|
||||
TAURI_WEBVIEW_REF
|
||||
};
|
||||
|
||||
util::open_link(url).map(|_| ()).ok_or_else(|| {
|
||||
WebViewError::UrlOpen(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Failed to open browser to WebView download section",
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn install() -> Result<(), WebViewError> {
|
||||
const WEBVIEW2_BOOTSTRAPPER_URL: &str = "https://go.microsoft.com/fwlink/p/?LinkId=2124703";
|
||||
const DEFAULT_FILENAME: &str = "MicrosoftEdgeWebview2Setup.exe";
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.user_agent("Hoppscotch Agent")
|
||||
.gzip(true)
|
||||
.build()?;
|
||||
|
||||
let response = client.get(WEBVIEW2_BOOTSTRAPPER_URL).send().await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(WebViewError::Download(format!(
|
||||
"Failed to download WebView2 bootstrapper. Status: {}",
|
||||
response.status()
|
||||
)));
|
||||
}
|
||||
|
||||
let filename =
|
||||
get_filename_from_response(&response).unwrap_or_else(|| DEFAULT_FILENAME.to_owned());
|
||||
|
||||
let tmp_dir = TempDir::with_prefix("WebView-setup-")?;
|
||||
let installer_path = tmp_dir.path().join(filename);
|
||||
|
||||
let content = response.bytes().await?;
|
||||
{
|
||||
let mut file = std::fs::File::create(&installer_path)?;
|
||||
io::copy(&mut Cursor::new(content), &mut file)?;
|
||||
}
|
||||
|
||||
let status = Command::new(&installer_path).args(["/install"]).status()?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(WebViewError::Installation(format!(
|
||||
"Installer exited with code `{}`.",
|
||||
status.code().unwrap_or(-1)
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn get_filename_from_response(response: &reqwest::Response) -> Option<String> {
|
||||
response
|
||||
.headers()
|
||||
.get("content-disposition")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(|value| value.split("filename=").last())
|
||||
.map(|name| name.trim().replace('\"', ""))
|
||||
.or_else(|| {
|
||||
response
|
||||
.url()
|
||||
.path_segments()
|
||||
.and_then(|segments| segments.last())
|
||||
.map(|name| name.to_string())
|
||||
})
|
||||
.filter(|name| !name.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
async fn install() -> Result<(), WebViewError> {
|
||||
Err(WebViewError::Installation(
|
||||
"Unable to auto-install WebView. Please refer to https://v2.tauri.app/references/webview-versions/".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn init_webview() {
|
||||
if is_available() {
|
||||
return;
|
||||
}
|
||||
|
||||
if dialog::confirm(
|
||||
"WebView Error",
|
||||
"WebView is required for this application to work.\n\n\
|
||||
Do you want to install it?",
|
||||
MessageType::Error,
|
||||
)
|
||||
.not()
|
||||
{
|
||||
tracing::warn!("Declined to setup WebView.");
|
||||
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if let Err(e) = tauri::async_runtime::block_on(install()) {
|
||||
dialog::error(&format!(
|
||||
"Failed to install WebView: {}\n\n\
|
||||
Please install it manually from webpage that should open when you click 'Ok'.\n\n\
|
||||
If that doesn't work, please visit Microsoft Edge Webview2 download section.",
|
||||
e
|
||||
));
|
||||
|
||||
if let Err(e) = open_install_website() {
|
||||
tracing::warn!("Failed to launch WebView website:\n{}", e);
|
||||
}
|
||||
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if is_available().not() {
|
||||
dialog::panic(
|
||||
"Unable to setup WebView:\n\n\
|
||||
Please install it manually and relaunch the application.\n\
|
||||
https://developer.microsoft.com/microsoft-edge/webview2/#download-section",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -131,7 +131,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from "vue"
|
||||
import { LazyStore } from "@tauri-apps/plugin-store"
|
||||
import { load } from "@hoppscotch/plugin-appload"
|
||||
import { load, close } from "@hoppscotch/plugin-appload"
|
||||
import { getVersion } from "@tauri-apps/api/app"
|
||||
|
||||
import { UpdateStatus, CheckResult, UpdateState } from "~/types"
|
||||
|
|
@ -341,6 +341,11 @@ const loadVendored = async () => {
|
|||
}
|
||||
|
||||
console.log("Vendored app loaded successfully")
|
||||
|
||||
console.log("Closing main window")
|
||||
|
||||
// NOTE: No need to await the promise here.
|
||||
close({ windowLabel: 'main' })
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err)
|
||||
console.error("Error loading vendored app:", errorMessage)
|
||||
|
|
|
|||
|
|
@ -980,8 +980,8 @@ importers:
|
|||
specifier: workspace:^
|
||||
version: link:../hoppscotch-kernel
|
||||
'@hoppscotch/plugin-appload':
|
||||
specifier: github:CuriousCorrelation/tauri-plugin-appload#e8dbe06eabf947e5efaf07d2e573238ceb11a7b1
|
||||
version: '@CuriousCorrelation/plugin-appload@https://codeload.github.com/CuriousCorrelation/tauri-plugin-appload/tar.gz/e8dbe06eabf947e5efaf07d2e573238ceb11a7b1'
|
||||
specifier: github:CuriousCorrelation/tauri-plugin-appload#feat-desktop-appload-top-level-config
|
||||
version: '@CuriousCorrelation/plugin-appload@https://codeload.github.com/CuriousCorrelation/tauri-plugin-appload/tar.gz/3d03f6c7b1726ecf179af2124e77a9742d600637'
|
||||
'@hoppscotch/ui':
|
||||
specifier: 0.2.5
|
||||
version: 0.2.5(eslint@8.57.0)(terser@5.39.2)(typescript@5.9.2)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(sass@1.91.0)(terser@5.39.2)(yaml@2.8.1))(vue@3.5.20(typescript@5.9.2))
|
||||
|
|
@ -1105,7 +1105,7 @@ importers:
|
|||
packages/hoppscotch-desktop/plugin-workspace/tauri-plugin-appload/examples/tauri-app:
|
||||
dependencies:
|
||||
'@tauri-apps/api':
|
||||
specifier: ^2.1.1
|
||||
specifier: 2.1.1
|
||||
version: 2.1.1
|
||||
tauri-plugin-appload-api:
|
||||
specifier: file:../../
|
||||
|
|
@ -1848,6 +1848,10 @@ packages:
|
|||
graphql:
|
||||
optional: true
|
||||
|
||||
'@CuriousCorrelation/plugin-appload@https://codeload.github.com/CuriousCorrelation/tauri-plugin-appload/tar.gz/3d03f6c7b1726ecf179af2124e77a9742d600637':
|
||||
resolution: {tarball: https://codeload.github.com/CuriousCorrelation/tauri-plugin-appload/tar.gz/3d03f6c7b1726ecf179af2124e77a9742d600637}
|
||||
version: 0.1.0
|
||||
|
||||
'@CuriousCorrelation/plugin-appload@https://codeload.github.com/CuriousCorrelation/tauri-plugin-appload/tar.gz/e8dbe06eabf947e5efaf07d2e573238ceb11a7b1':
|
||||
resolution: {tarball: https://codeload.github.com/CuriousCorrelation/tauri-plugin-appload/tar.gz/e8dbe06eabf947e5efaf07d2e573238ceb11a7b1}
|
||||
version: 0.1.0
|
||||
|
|
@ -15533,6 +15537,10 @@ snapshots:
|
|||
optionalDependencies:
|
||||
graphql: 16.11.0
|
||||
|
||||
'@CuriousCorrelation/plugin-appload@https://codeload.github.com/CuriousCorrelation/tauri-plugin-appload/tar.gz/3d03f6c7b1726ecf179af2124e77a9742d600637':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.1.1
|
||||
|
||||
'@CuriousCorrelation/plugin-appload@https://codeload.github.com/CuriousCorrelation/tauri-plugin-appload/tar.gz/e8dbe06eabf947e5efaf07d2e573238ceb11a7b1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.1.1
|
||||
|
|
|
|||
Loading…
Reference in a new issue