Merge remote-tracking branch 'origin/v2' into feat/updater/refactor

pull/431/head
Lucas Nogueira 2 years ago
commit 5040d1f449
No known key found for this signature in database
GPG Key ID: FFEA6C72E73482F1

@ -0,0 +1,5 @@
---
"dialog": "patch"
---
On non-Linux system, use `AsyncMessageDialog` instead of `MessageDialog`. [(tauri#7182)](https://github.com/tauri-apps/tauri/issues/7182)

@ -0,0 +1,5 @@
---
"fs-js": patch
---
Fix `writeBinaryFile` crashing with `command 'write_binary_file' not found`

@ -0,0 +1,5 @@
---
"http-js": minor
---
Multipart requests are now handled in JavaScript by the `Request` JavaScript class so you just need to use a `FormData` body and not set the content-type header to `multipart/form-data`. `application/x-www-form-urlencoded` requests must be done manually.

@ -0,0 +1,6 @@
---
"http": minor
"http-js": minor
---
The http plugin has been rewritten from scratch and now only exposes a `fetch` function in Javascript and Re-exports `reqwest` crate in Rust. The new `fetch` method tries to be as close and compliant to the `fetch` Web API as possible.

@ -0,0 +1,5 @@
---
"persisted-scope": patch
---
Split up fs and asset scopes. **This will reset the asset protocol scope once!**

@ -0,0 +1,5 @@
---
"persisted-scope": patch
---
Fix usage of directory patterns by removing glob asterisks at the end before allowing/forbidding them. This was causing them to be escaped, and so undesirable paths were allowed/forbidden while polluting the `.persisted-scope` file.

@ -0,0 +1,5 @@
---
"shell": "patch"
---
Added `Command::arg`, `Command::env` and changed `Command::new` input type.

@ -0,0 +1,5 @@
---
"shell": patch
---
Ensure the launched process is detached so it can out-live your tauri app and does not shutdown with it.

@ -0,0 +1,5 @@
---
"stronghold-js": patch
---
Change the argument name of the `Stronghold.remove` from `location` to `recordPath` to match the Stronghold command argument

@ -0,0 +1,10 @@
---
"window": "patch"
"window-js": "patch"
---
The window plugin is recieving a few changes to improve consistency and add new features:
- Removed `appWindow` variable from JS module, use `getCurrent` or `Window.getCurrent`.
- Removed `WindowManager`, `WebviewWindow` and `WebviewHandle` types and merged them into one `Window` type that matches the name of the rust window type.
- Added `Window.getCurrent` and `Window.getAll` which is a convenient method for `getCurrent` and `getAll` functions.

@ -116,6 +116,6 @@ if (files.length > 0) {
console.log(missing.join("\n")); console.log(missing.join("\n"));
process.exit(1); process.exit(1);
} }
} },
); );
} }

@ -52,7 +52,7 @@ https.get(url, options, (response) => {
} }
} else if (kind === "npm") { } else if (kind === "npm") {
const versions = Object.keys(data.versions || {}).filter((v) => const versions = Object.keys(data.versions || {}).filter((v) =>
v.startsWith(target) v.startsWith(target),
); );
console.log(versions[versions.length - 1] || "0.0.0"); console.log(versions[versions.length - 1] || "0.0.0");
} }

1678
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en" theme="dark"> <html lang="en" theme="dark">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />

@ -4,7 +4,7 @@ publish = false
version = "2.0.0-alpha.1" version = "2.0.0-alpha.1"
description = "An example Tauri Application showcasing the api" description = "An example Tauri Application showcasing the api"
edition = "2021" edition = "2021"
rust-version = "1.65" rust-version = { workspace = true }
license = "Apache-2.0 OR MIT" license = "Apache-2.0 OR MIT"
[lib] [lib]

@ -1,7 +1,7 @@
<script> <script>
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import { open } from "@tauri-apps/plugin-shell"; import { open } from "@tauri-apps/plugin-shell";
import { appWindow, getCurrent } from "@tauri-apps/plugin-window"; import { getCurrent } from "@tauri-apps/plugin-window";
import * as os from "@tauri-apps/plugin-os"; import * as os from "@tauri-apps/plugin-os";
import Welcome from "./views/Welcome.svelte"; import Welcome from "./views/Welcome.svelte";
@ -22,6 +22,8 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { ask } from "@tauri-apps/plugin-dialog"; import { ask } from "@tauri-apps/plugin-dialog";
const appWindow = getCurrent();
if (appWindow.label !== "main") { if (appWindow.label !== "main") {
appWindow.onCloseRequested(async (event) => { appWindow.onCloseRequested(async (event) => {
const confirmed = await confirm("Are you sure?"); const confirmed = await confirm("Are you sure?");
@ -121,20 +123,20 @@
// Window controls // Window controls
let isWindowMaximized; let isWindowMaximized;
onMount(async () => { onMount(async () => {
const window = getCurrent(); isWindowMaximized = await appWindow.isMaximized();
isWindowMaximized = await window.isMaximized(); appWindow.onResized(async () => {
window.onResized(async () => { isWindowMaximized = await appWindow.isMaximized();
isWindowMaximized = await window.isMaximized();
}); });
}); });
function minimize() { function minimize() {
getCurrent().minimize(); appWindow.minimize();
} }
async function toggleMaximize() { async function toggleMaximize() {
const window = getCurrent(); (await appWindow.isMaximized())
(await window.isMaximized()) ? window.unmaximize() : window.maximize(); ? appWindow.unmaximize()
: appWindow.maximize();
} }
let confirmed_close = false; let confirmed_close = false;
@ -147,7 +149,7 @@
} }
); );
if (confirmed_close) { if (confirmed_close) {
getCurrent().close(); appWindow.close();
} }
} }
} }

@ -1,8 +1,10 @@
<script> <script>
import { appWindow } from "@tauri-apps/plugin-window"; import { getCurrent } from "@tauri-apps/plugin-window";
import { invoke } from "@tauri-apps/api/tauri"; import { invoke } from "@tauri-apps/api/tauri";
import { onMount, onDestroy } from "svelte"; import { onMount, onDestroy } from "svelte";
const appWindow = getCurrent();
export let onMessage; export let onMessage;
let unlisten; let unlisten;

@ -1,5 +1,5 @@
<script> <script>
import { getClient, Body, ResponseType } from "@tauri-apps/plugin-http"; import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
import { JsonView } from "@zerodevx/svelte-json-view"; import { JsonView } from "@zerodevx/svelte-json-view";
let httpMethod = "GET"; let httpMethod = "GET";
@ -8,53 +8,62 @@
export let onMessage; export let onMessage;
async function makeHttpRequest() { async function makeHttpRequest() {
const client = await getClient().catch((e) => {
onMessage(e);
throw e;
});
let method = httpMethod || "GET"; let method = httpMethod || "GET";
const options = { const options = {
url: "http://localhost:3003",
method: method || "GET", method: method || "GET",
headers: {},
}; };
if ( let bodyType;
(httpBody.startsWith("{") && httpBody.endsWith("}")) ||
(httpBody.startsWith("[") && httpBody.endsWith("]")) if (method !== "GET") {
) { options.body = httpBody;
options.body = Body.json(JSON.parse(httpBody));
} else if (httpBody !== "") { if (
options.body = Body.text(httpBody); (httpBody.startsWith("{") && httpBody.endsWith("}")) ||
(httpBody.startsWith("[") && httpBody.endsWith("]"))
) {
options.headers["Content-Type"] = "application/json";
bodyType = "json";
} else if (httpBody !== "") {
bodyType = "text";
}
} }
client.request(options).then(onMessage).catch(onMessage); const response = await tauriFetch("http://localhost:3003", options);
const body =
bodyType === "json" ? await response.json() : await response.text();
onMessage({
url: response.url,
status: response.status,
ok: response.ok,
headers: Object.fromEntries(response.headers.entries()),
body,
});
} }
/// http form /// http form
let foo = "baz"; let foo = "baz";
let bar = "qux"; let bar = "qux";
let result = null; let result = null;
let multipart = true;
async function doPost() { async function doPost() {
const client = await getClient().catch((e) => { const form = new FormData();
onMessage(e); form.append("foo", foo);
throw e; form.append("bar", bar);
}); const response = await tauriFetch("http://localhost:3003", {
result = await client.request({
url: "http://localhost:3003",
method: "POST", method: "POST",
body: Body.form({ body: form,
foo,
bar,
}),
headers: multipart
? { "Content-Type": "multipart/form-data" }
: undefined,
responseType: ResponseType.Text,
}); });
result = {
url: response.url,
status: response.status,
ok: response.ok,
headers: Object.fromEntries(response.headers.entries()),
body: await response.text(),
};
} }
</script> </script>
@ -87,11 +96,6 @@
<input class="input" bind:value={bar} /> <input class="input" bind:value={bar} />
</div> </div>
<br /> <br />
<label>
<input type="checkbox" bind:checked={multipart} />
Multipart
</label>
<br />
<br /> <br />
<button class="btn" type="button" on:click={doPost}> Post it</button> <button class="btn" type="button" on:click={doPost}> Post it</button>
<br /> <br />

@ -1,17 +1,19 @@
<script> <script>
import { import {
appWindow, getCurrent,
WebviewWindow,
LogicalSize, LogicalSize,
UserAttentionType, UserAttentionType,
PhysicalSize, PhysicalSize,
PhysicalPosition, PhysicalPosition,
Effect, Effect,
EffectState, EffectState,
Window
} from "@tauri-apps/plugin-window"; } from "@tauri-apps/plugin-window";
import { open as openDialog } from "@tauri-apps/plugin-dialog"; import { open as openDialog } from "@tauri-apps/plugin-dialog";
import { open } from "@tauri-apps/plugin-shell"; import { open } from "@tauri-apps/plugin-shell";
const appWindow = getCurrent();
let selectedWindow = appWindow.label; let selectedWindow = appWindow.label;
const windowMap = { const windowMap = {
[appWindow.label]: appWindow, [appWindow.label]: appWindow,
@ -146,7 +148,7 @@
function createWindow() { function createWindow() {
if (!newWindowLabel) return; if (!newWindowLabel) return;
const webview = new WebviewWindow(newWindowLabel); const webview = new Window(newWindowLabel);
windowMap[newWindowLabel] = webview; windowMap[newWindowLabel] = webview;
webview.once("tauri://error", function () { webview.once("tauri://error", function () {
onMessage("Error creating new webview"); onMessage("Error creating new webview");

@ -10,24 +10,28 @@
"format-check": "prettier --check ." "format-check": "prettier --check ."
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-node-resolve": "^15.1.0", "@rollup/plugin-node-resolve": "15.1.0",
"@rollup/plugin-terser": "^0.4.3", "@rollup/plugin-terser": "0.4.3",
"@rollup/plugin-typescript": "^11.1.1", "@rollup/plugin-typescript": "11.1.2",
"@typescript-eslint/eslint-plugin": "^5.59.11", "@typescript-eslint/eslint-plugin": "6.1.0",
"@typescript-eslint/parser": "^5.59.11", "@typescript-eslint/parser": "6.1.0",
"covector": "^0.9.0", "covector": "^0.9.0",
"eslint": "^8.43.0", "eslint": "8.45.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "8.8.0",
"eslint-config-standard-with-typescript": "^35.0.0", "eslint-config-standard-with-typescript": "36.1.0",
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "2.27.5",
"eslint-plugin-n": "^16.0.0", "eslint-plugin-n": "16.0.1",
"eslint-plugin-promise": "^6.1.1", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-security": "^1.7.1", "eslint-plugin-security": "1.7.1",
"prettier": "^2.8.8", "prettier": "3.0.0",
"rollup": "^3.25.1", "rollup": "3.26.3",
"typescript": "^5.1.3" "typescript": "5.1.6"
},
"resolutions": {
"semver": ">=7.5.2",
"optionator": ">=0.9.3"
}, },
"engines": { "engines": {
"pnpm": ">=7.33.0" "pnpm": ">=7.33.1"
} }
} }

@ -1,4 +1,4 @@
![plugin-app](banner.png) ![plugin-app](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/app/banner.png)
This plugin provides APIs to read application metadata and macOS app visibility functions. This plugin provides APIs to read application metadata and macOS app visibility functions.

@ -5,7 +5,7 @@ import { createConfig } from "../../shared/rollup.config.mjs";
export default createConfig({ export default createConfig({
input: "guest-js/index.ts", input: "guest-js/index.ts",
pkg: JSON.parse( pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8") readFileSync(new URL("./package.json", import.meta.url), "utf8"),
), ),
external: [/^@tauri-apps\/api/], external: [/^@tauri-apps\/api/],
}); });

@ -2,6 +2,15 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/app/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/app)
//!
//! This plugin provides APIs to read application metadata and macOS app visibility functions.
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
use tauri::{ use tauri::{
plugin::{Builder, TauriPlugin}, plugin::{Builder, TauriPlugin},
Runtime, Runtime,

@ -1,4 +1,4 @@
![plugin-authenticator](banner.png) ![plugin-authenticator](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/authenticator/banner.png)
Use hardware security-keys in your Tauri App. Use hardware security-keys in your Tauri App.
@ -93,7 +93,7 @@ const r2 = await auth.verifyRegistration(
challenge, challenge,
app, app,
registerResult.registerData, registerResult.registerData,
registerResult.clientData registerResult.clientData,
); );
const j2 = JSON.parse(r2); const j2 = JSON.parse(r2);
@ -108,7 +108,7 @@ const counter = await auth.verifySignature(
signData.signData, signData.signData,
clientData, clientData,
keyHandle, keyHandle,
pubkey pubkey,
); );
if (counter && counter > 0) { if (counter && counter > 0) {

@ -25,7 +25,7 @@ export class Authenticator {
challenge: string, challenge: string,
application: string, application: string,
registerData: string, registerData: string,
clientData: string clientData: string,
): Promise<string> { ): Promise<string> {
return await window.__TAURI_INVOKE__( return await window.__TAURI_INVOKE__(
"plugin:authenticator|verify_registration", "plugin:authenticator|verify_registration",
@ -34,14 +34,14 @@ export class Authenticator {
application, application,
registerData, registerData,
clientData, clientData,
} },
); );
} }
async sign( async sign(
challenge: string, challenge: string,
application: string, application: string,
keyHandle: string keyHandle: string,
): Promise<string> { ): Promise<string> {
return await window.__TAURI_INVOKE__("plugin:authenticator|sign", { return await window.__TAURI_INVOKE__("plugin:authenticator|sign", {
timeout: 10000, timeout: 10000,
@ -57,7 +57,7 @@ export class Authenticator {
signData: string, signData: string,
clientData: string, clientData: string,
keyHandle: string, keyHandle: string,
pubkey: string pubkey: string,
): Promise<number> { ): Promise<number> {
return await window.__TAURI_INVOKE__( return await window.__TAURI_INVOKE__(
"plugin:authenticator|verify_signature", "plugin:authenticator|verify_signature",
@ -68,7 +68,7 @@ export class Authenticator {
clientData, clientData,
keyHandle, keyHandle,
pubkey, pubkey,
} },
); );
} }
} }

@ -25,7 +25,7 @@
"LICENSE" "LICENSE"
], ],
"devDependencies": { "devDependencies": {
"tslib": "^2.5.0" "tslib": "2.6.0"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.5" "@tauri-apps/api": "2.0.0-alpha.5"

@ -5,7 +5,7 @@ import { createConfig } from "../../shared/rollup.config.mjs";
export default createConfig({ export default createConfig({
input: "guest-js/index.ts", input: "guest-js/index.ts",
pkg: JSON.parse( pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8") readFileSync(new URL("./package.json", import.meta.url), "utf8"),
), ),
external: [/^@tauri-apps\/api/], external: [/^@tauri-apps\/api/],
}); });

@ -2,6 +2,16 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/authenticator/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/authenticator)
//!
//! Use hardware security-keys in your Tauri App.
//!
//! - Supported platforms: Windows, Linux, FreeBSD, NetBSD, OpenBSD, and macOS.
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
#![cfg(not(any(target_os = "android", target_os = "ios")))] #![cfg(not(any(target_os = "android", target_os = "ios")))]
mod auth; mod auth;

@ -1,4 +1,4 @@
![plugin-autostart](banner.png) ![plugin-autostart](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/autostart/banner.png)
Automatically launch your application at startup. Supports Windows, Mac (via AppleScript or Launch Agent), and Linux. Automatically launch your application at startup. Supports Windows, Mac (via AppleScript or Launch Agent), and Linux.

@ -24,7 +24,7 @@
"LICENSE" "LICENSE"
], ],
"devDependencies": { "devDependencies": {
"tslib": "^2.5.0" "tslib": "2.6.0"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.5" "@tauri-apps/api": "2.0.0-alpha.5"

@ -5,7 +5,7 @@ import { createConfig } from "../../shared/rollup.config.mjs";
export default createConfig({ export default createConfig({
input: "guest-js/index.ts", input: "guest-js/index.ts",
pkg: JSON.parse( pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8") readFileSync(new URL("./package.json", import.meta.url), "utf8"),
), ),
external: [/^@tauri-apps\/api/], external: [/^@tauri-apps\/api/],
}); });

@ -2,6 +2,14 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/autostart/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/autostart)
//!
//! Automatically launch your application at startup. Supports Windows, Mac (via AppleScript or Launch Agent), and Linux.
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
#![cfg(not(any(target_os = "android", target_os = "ios")))] #![cfg(not(any(target_os = "android", target_os = "ios")))]
use auto_launch::{AutoLaunch, AutoLaunchBuilder}; use auto_launch::{AutoLaunch, AutoLaunchBuilder};

@ -1,4 +1,4 @@
![plugin-cli](banner.png) ![plugin-cli](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/cli/banner.png)
Parse arguments from your Command Line Interface. Parse arguments from your Command Line Interface.

@ -5,7 +5,7 @@ import { createConfig } from "../../shared/rollup.config.mjs";
export default createConfig({ export default createConfig({
input: "guest-js/index.ts", input: "guest-js/index.ts",
pkg: JSON.parse( pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8") readFileSync(new URL("./package.json", import.meta.url), "utf8"),
), ),
external: [/^@tauri-apps\/api/], external: [/^@tauri-apps\/api/],
}); });

@ -2,6 +2,17 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/cli/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/cli)
//!
//! Parse arguments from your Command Line Interface.
//!
//! - Supported platforms: Windows, Linux and macOS.
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
use tauri::{ use tauri::{
plugin::{Builder, PluginApi, TauriPlugin}, plugin::{Builder, PluginApi, TauriPlugin},
AppHandle, Manager, Runtime, State, AppHandle, Manager, Runtime, State,

@ -1,4 +1,4 @@
![plugin-clipboard-manager](banner.png) ![plugin-clipboard-manager](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/clipboard-manager/banner.png)
Read and write to the system clipboard. Read and write to the system clipboard.

@ -36,7 +36,7 @@ type ClipResponse = Clip<"PlainText", string>;
*/ */
async function writeText( async function writeText(
text: string, text: string,
opts?: { label?: string } opts?: { label?: string },
): Promise<void> { ): Promise<void> {
return window.__TAURI_INVOKE__("plugin:clipboard|write", { return window.__TAURI_INVOKE__("plugin:clipboard|write", {
data: { data: {
@ -60,7 +60,7 @@ async function writeText(
*/ */
async function readText(): Promise<string> { async function readText(): Promise<string> {
const kind: ClipResponse = await window.__TAURI_INVOKE__( const kind: ClipResponse = await window.__TAURI_INVOKE__(
"plugin:clipboard|read" "plugin:clipboard|read",
); );
return kind.options; return kind.options;
} }

@ -5,7 +5,7 @@ import { createConfig } from "../../shared/rollup.config.mjs";
export default createConfig({ export default createConfig({
input: "guest-js/index.ts", input: "guest-js/index.ts",
pkg: JSON.parse( pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8") readFileSync(new URL("./package.json", import.meta.url), "utf8"),
), ),
external: [/^@tauri-apps\/api/], external: [/^@tauri-apps\/api/],
}); });

@ -2,6 +2,15 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/clipboard-manager/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/clipboard-manager)
//!
//! Read and write to the system clipboard.
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
use tauri::{ use tauri::{
plugin::{Builder, TauriPlugin}, plugin::{Builder, TauriPlugin},
Manager, Runtime, Manager, Runtime,

@ -1,4 +1,4 @@
![plugin-dialog](banner.png) ![plugin-dialog](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/dialog/banner.png)
Native system dialogs for opening and saving files along with message dialogs. Native system dialogs for opening and saving files along with message dialogs.

@ -103,16 +103,16 @@ interface ConfirmDialogOptions {
} }
async function open( async function open(
options?: OpenDialogOptions & { multiple?: false; directory?: false } options?: OpenDialogOptions & { multiple?: false; directory?: false },
): Promise<null | FileResponse>; ): Promise<null | FileResponse>;
async function open( async function open(
options?: OpenDialogOptions & { multiple?: true; directory?: false } options?: OpenDialogOptions & { multiple?: true; directory?: false },
): Promise<null | FileResponse[]>; ): Promise<null | FileResponse[]>;
async function open( async function open(
options?: OpenDialogOptions & { multiple?: false; directory?: true } options?: OpenDialogOptions & { multiple?: false; directory?: true },
): Promise<null | string>; ): Promise<null | string>;
async function open( async function open(
options?: OpenDialogOptions & { multiple?: true; directory?: true } options?: OpenDialogOptions & { multiple?: true; directory?: true },
): Promise<null | string[]>; ): Promise<null | string[]>;
/** /**
* Open a file/directory selection dialog. * Open a file/directory selection dialog.
@ -125,7 +125,7 @@ async function open(
* You can save it to the filesystem using [tauri-plugin-persisted-scope](https://github.com/tauri-apps/tauri-plugin-persisted-scope). * You can save it to the filesystem using [tauri-plugin-persisted-scope](https://github.com/tauri-apps/tauri-plugin-persisted-scope).
* @example * @example
* ```typescript * ```typescript
* import { open } from '@tauri-apps/api/dialog'; * import { open } from '@tauri-apps/plugin-dialog';
* // Open a selection dialog for image files * // Open a selection dialog for image files
* const selected = await open({ * const selected = await open({
* multiple: true, * multiple: true,
@ -145,7 +145,7 @@ async function open(
* *
* @example * @example
* ```typescript * ```typescript
* import { open } from '@tauri-apps/api/dialog'; * import { open } from '@tauri-apps/plugin-dialog';
* import { appDir } from '@tauri-apps/api/path'; * import { appDir } from '@tauri-apps/api/path';
* // Open a selection dialog for directories * // Open a selection dialog for directories
* const selected = await open({ * const selected = await open({
@ -167,7 +167,7 @@ async function open(
* @since 2.0.0 * @since 2.0.0
*/ */
async function open( async function open(
options: OpenDialogOptions = {} options: OpenDialogOptions = {},
): Promise<null | string | string[] | FileResponse | FileResponse[]> { ): Promise<null | string | string[] | FileResponse | FileResponse[]> {
if (typeof options === "object") { if (typeof options === "object") {
Object.freeze(options); Object.freeze(options);
@ -187,7 +187,7 @@ async function open(
* You can save it to the filesystem using [tauri-plugin-persisted-scope](https://github.com/tauri-apps/tauri-plugin-persisted-scope). * You can save it to the filesystem using [tauri-plugin-persisted-scope](https://github.com/tauri-apps/tauri-plugin-persisted-scope).
* @example * @example
* ```typescript * ```typescript
* import { save } from '@tauri-apps/api/dialog'; * import { save } from '@tauri-apps/plugin-dialog';
* const filePath = await save({ * const filePath = await save({
* filters: [{ * filters: [{
* name: 'Image', * name: 'Image',
@ -212,7 +212,7 @@ async function save(options: SaveDialogOptions = {}): Promise<string | null> {
* Shows a message dialog with an `Ok` button. * Shows a message dialog with an `Ok` button.
* @example * @example
* ```typescript * ```typescript
* import { message } from '@tauri-apps/api/dialog'; * import { message } from '@tauri-apps/plugin-dialog';
* await message('Tauri is awesome', 'Tauri'); * await message('Tauri is awesome', 'Tauri');
* await message('File not found', { title: 'Tauri', type: 'error' }); * await message('File not found', { title: 'Tauri', type: 'error' });
* ``` * ```
@ -227,7 +227,7 @@ async function save(options: SaveDialogOptions = {}): Promise<string | null> {
*/ */
async function message( async function message(
message: string, message: string,
options?: string | MessageDialogOptions options?: string | MessageDialogOptions,
): Promise<void> { ): Promise<void> {
const opts = typeof options === "string" ? { title: options } : options; const opts = typeof options === "string" ? { title: options } : options;
return window.__TAURI_INVOKE__("plugin:dialog|message", { return window.__TAURI_INVOKE__("plugin:dialog|message", {
@ -242,7 +242,7 @@ async function message(
* Shows a question dialog with `Yes` and `No` buttons. * Shows a question dialog with `Yes` and `No` buttons.
* @example * @example
* ```typescript * ```typescript
* import { ask } from '@tauri-apps/api/dialog'; * import { ask } from '@tauri-apps/plugin-dialog';
* const yes = await ask('Are you sure?', 'Tauri'); * const yes = await ask('Are you sure?', 'Tauri');
* const yes2 = await ask('This action cannot be reverted. Are you sure?', { title: 'Tauri', type: 'warning' }); * const yes2 = await ask('This action cannot be reverted. Are you sure?', { title: 'Tauri', type: 'warning' });
* ``` * ```
@ -256,7 +256,7 @@ async function message(
*/ */
async function ask( async function ask(
message: string, message: string,
options?: string | ConfirmDialogOptions options?: string | ConfirmDialogOptions,
): Promise<boolean> { ): Promise<boolean> {
const opts = typeof options === "string" ? { title: options } : options; const opts = typeof options === "string" ? { title: options } : options;
return window.__TAURI_INVOKE__("plugin:dialog|ask", { return window.__TAURI_INVOKE__("plugin:dialog|ask", {
@ -272,7 +272,7 @@ async function ask(
* Shows a question dialog with `Ok` and `Cancel` buttons. * Shows a question dialog with `Ok` and `Cancel` buttons.
* @example * @example
* ```typescript * ```typescript
* import { confirm } from '@tauri-apps/api/dialog'; * import { confirm } from '@tauri-apps/plugin-dialog';
* const confirmed = await confirm('Are you sure?', 'Tauri'); * const confirmed = await confirm('Are you sure?', 'Tauri');
* const confirmed2 = await confirm('This action cannot be reverted. Are you sure?', { title: 'Tauri', type: 'warning' }); * const confirmed2 = await confirm('This action cannot be reverted. Are you sure?', { title: 'Tauri', type: 'warning' });
* ``` * ```
@ -286,7 +286,7 @@ async function ask(
*/ */
async function confirm( async function confirm(
message: string, message: string,
options?: string | ConfirmDialogOptions options?: string | ConfirmDialogOptions,
): Promise<boolean> { ): Promise<boolean> {
const opts = typeof options === "string" ? { title: options } : options; const opts = typeof options === "string" ? { title: options } : options;
return window.__TAURI_INVOKE__("plugin:dialog|confirm", { return window.__TAURI_INVOKE__("plugin:dialog|confirm", {

@ -5,7 +5,7 @@ import { createConfig } from "../../shared/rollup.config.mjs";
export default createConfig({ export default createConfig({
input: "guest-js/index.ts", input: "guest-js/index.ts",
pkg: JSON.parse( pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8") readFileSync(new URL("./package.json", import.meta.url), "utf8"),
), ),
external: [/^@tauri-apps\/api/], external: [/^@tauri-apps\/api/],
}); });

@ -23,6 +23,11 @@ type FileDialog = rfd::FileDialog;
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
type FileDialog = rfd::AsyncFileDialog; type FileDialog = rfd::AsyncFileDialog;
#[cfg(target_os = "linux")]
type MessageDialog = rfd::MessageDialog;
#[cfg(not(target_os = "linux"))]
type MessageDialog = rfd::AsyncMessageDialog;
pub fn init<R: Runtime, C: DeserializeOwned>( pub fn init<R: Runtime, C: DeserializeOwned>(
app: &AppHandle<R>, app: &AppHandle<R>,
_api: PluginApi<R, C>, _api: PluginApi<R, C>,
@ -50,7 +55,7 @@ impl<R: Runtime> Dialog<R> {
macro_rules! run_dialog { macro_rules! run_dialog {
($e:expr, $h: ident) => {{ ($e:expr, $h: ident) => {{
std::thread::spawn(move || { std::thread::spawn(move || {
let response = $e; let response = tauri::async_runtime::block_on($e);
$h(response); $h(response);
}); });
}}; }};
@ -136,9 +141,9 @@ impl<R: Runtime> From<FileDialogBuilder<R>> for FileDialog {
} }
} }
impl<R: Runtime> From<MessageDialogBuilder<R>> for rfd::MessageDialog { impl<R: Runtime> From<MessageDialogBuilder<R>> for MessageDialog {
fn from(d: MessageDialogBuilder<R>) -> Self { fn from(d: MessageDialogBuilder<R>) -> Self {
let mut dialog = rfd::MessageDialog::new() let mut dialog = MessageDialog::new()
.set_title(&d.title) .set_title(&d.title)
.set_description(&d.message) .set_description(&d.message)
.set_level(d.kind.into()); .set_level(d.kind.into());
@ -215,5 +220,5 @@ pub fn show_message_dialog<R: Runtime, F: FnOnce(bool) + Send + 'static>(
dialog: MessageDialogBuilder<R>, dialog: MessageDialogBuilder<R>,
f: F, f: F,
) { ) {
run_dialog!(rfd::MessageDialog::from(dialog).show(), f); run_dialog!(MessageDialog::from(dialog).show(), f);
} }

@ -2,6 +2,15 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/dialog/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/dialog)
//!
//! Native system dialogs for opening and saving files along with message dialogs.
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::{ use tauri::{
plugin::{Builder, TauriPlugin}, plugin::{Builder, TauriPlugin},
@ -200,7 +209,9 @@ impl<R: Runtime> MessageDialogBuilder<R> {
show_message_dialog(self, f) show_message_dialog(self, f)
} }
//// Shows a message dialog. /// Shows a message dialog.
/// This is a blocking operation,
/// and should *NOT* be used when running on the main thread context.
pub fn blocking_show(self) -> bool { pub fn blocking_show(self) -> bool {
blocking_fn!(self, show) blocking_fn!(self, show)
} }

@ -1,4 +1,4 @@
![plugin-fs](banner.png) ![plugin-fs](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/fs/banner.png)
Access the file system. Access the file system.

@ -229,7 +229,7 @@ interface FileEntry {
*/ */
async function readTextFile( async function readTextFile(
filePath: string, filePath: string,
options: FsOptions = {} options: FsOptions = {},
): Promise<string> { ): Promise<string> {
return await window.__TAURI_INVOKE__("plugin:fs|read_text_file", { return await window.__TAURI_INVOKE__("plugin:fs|read_text_file", {
path: filePath, path: filePath,
@ -250,7 +250,7 @@ async function readTextFile(
*/ */
async function readBinaryFile( async function readBinaryFile(
filePath: string, filePath: string,
options: FsOptions = {} options: FsOptions = {},
): Promise<Uint8Array> { ): Promise<Uint8Array> {
const arr = await window.__TAURI_INVOKE__<number[]>("plugin:fs|read_file", { const arr = await window.__TAURI_INVOKE__<number[]>("plugin:fs|read_file", {
path: filePath, path: filePath,
@ -274,7 +274,7 @@ async function readBinaryFile(
async function writeTextFile( async function writeTextFile(
path: string, path: string,
contents: string, contents: string,
options?: FsOptions options?: FsOptions,
): Promise<void>; ): Promise<void>;
/** /**
@ -291,7 +291,7 @@ async function writeTextFile(
*/ */
async function writeTextFile( async function writeTextFile(
file: FsTextFileOption, file: FsTextFileOption,
options?: FsOptions options?: FsOptions,
): Promise<void>; ): Promise<void>;
/** /**
@ -304,7 +304,7 @@ async function writeTextFile(
async function writeTextFile( async function writeTextFile(
path: string | FsTextFileOption, path: string | FsTextFileOption,
contents?: string | FsOptions, contents?: string | FsOptions,
options?: FsOptions options?: FsOptions,
): Promise<void> { ): Promise<void> {
if (typeof options === "object") { if (typeof options === "object") {
Object.freeze(options); Object.freeze(options);
@ -352,7 +352,7 @@ async function writeTextFile(
async function writeBinaryFile( async function writeBinaryFile(
path: string, path: string,
contents: BinaryFileContents, contents: BinaryFileContents,
options?: FsOptions options?: FsOptions,
): Promise<void>; ): Promise<void>;
/** /**
@ -372,7 +372,7 @@ async function writeBinaryFile(
*/ */
async function writeBinaryFile( async function writeBinaryFile(
file: FsBinaryFileOption, file: FsBinaryFileOption,
options?: FsOptions options?: FsOptions,
): Promise<void>; ): Promise<void>;
/** /**
@ -385,7 +385,7 @@ async function writeBinaryFile(
async function writeBinaryFile( async function writeBinaryFile(
path: string | FsBinaryFileOption, path: string | FsBinaryFileOption,
contents?: BinaryFileContents | FsOptions, contents?: BinaryFileContents | FsOptions,
options?: FsOptions options?: FsOptions,
): Promise<void> { ): Promise<void> {
if (typeof options === "object") { if (typeof options === "object") {
Object.freeze(options); Object.freeze(options);
@ -410,12 +410,12 @@ async function writeBinaryFile(
file.contents = contents ?? []; file.contents = contents ?? [];
} }
return await window.__TAURI_INVOKE__("plugin:fs|write_binary_file", { return await window.__TAURI_INVOKE__("plugin:fs|write_file", {
path: file.path, path: file.path,
contents: Array.from( contents: Array.from(
file.contents instanceof ArrayBuffer file.contents instanceof ArrayBuffer
? new Uint8Array(file.contents) ? new Uint8Array(file.contents)
: file.contents : file.contents,
), ),
options: fileOptions, options: fileOptions,
}); });
@ -443,7 +443,7 @@ async function writeBinaryFile(
*/ */
async function readDir( async function readDir(
dir: string, dir: string,
options: FsDirOptions = {} options: FsDirOptions = {},
): Promise<FileEntry[]> { ): Promise<FileEntry[]> {
return await window.__TAURI_INVOKE__("plugin:fs|read_dir", { return await window.__TAURI_INVOKE__("plugin:fs|read_dir", {
path: dir, path: dir,
@ -468,7 +468,7 @@ async function readDir(
*/ */
async function createDir( async function createDir(
dir: string, dir: string,
options: FsDirOptions = {} options: FsDirOptions = {},
): Promise<void> { ): Promise<void> {
return await window.__TAURI_INVOKE__("plugin:fs|create_dir", { return await window.__TAURI_INVOKE__("plugin:fs|create_dir", {
path: dir, path: dir,
@ -492,7 +492,7 @@ async function createDir(
*/ */
async function removeDir( async function removeDir(
dir: string, dir: string,
options: FsDirOptions = {} options: FsDirOptions = {},
): Promise<void> { ): Promise<void> {
return await window.__TAURI_INVOKE__("plugin:fs|remove_dir", { return await window.__TAURI_INVOKE__("plugin:fs|remove_dir", {
path: dir, path: dir,
@ -516,7 +516,7 @@ async function removeDir(
async function copyFile( async function copyFile(
source: string, source: string,
destination: string, destination: string,
options: FsOptions = {} options: FsOptions = {},
): Promise<void> { ): Promise<void> {
return await window.__TAURI_INVOKE__("plugin:fs|copy_file", { return await window.__TAURI_INVOKE__("plugin:fs|copy_file", {
source, source,
@ -540,7 +540,7 @@ async function copyFile(
*/ */
async function removeFile( async function removeFile(
file: string, file: string,
options: FsOptions = {} options: FsOptions = {},
): Promise<void> { ): Promise<void> {
return await window.__TAURI_INVOKE__("plugin:fs|remove_file", { return await window.__TAURI_INVOKE__("plugin:fs|remove_file", {
path: file, path: file,
@ -564,7 +564,7 @@ async function removeFile(
async function renameFile( async function renameFile(
oldPath: string, oldPath: string,
newPath: string, newPath: string,
options: FsOptions = {} options: FsOptions = {},
): Promise<void> { ): Promise<void> {
return await window.__TAURI_INVOKE__("plugin:fs|rename_file", { return await window.__TAURI_INVOKE__("plugin:fs|rename_file", {
oldPath, oldPath,

@ -5,7 +5,7 @@ import { createConfig } from "../../shared/rollup.config.mjs";
export default createConfig({ export default createConfig({
input: "guest-js/index.ts", input: "guest-js/index.ts",
pkg: JSON.parse( pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8") readFileSync(new URL("./package.json", import.meta.url), "utf8"),
), ),
external: [/^@tauri-apps\/api/], external: [/^@tauri-apps\/api/],
}); });

File diff suppressed because one or more lines are too long

@ -2,6 +2,15 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/fs/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/fs)
//!
//! Access the file system.
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
use config::FsScope; use config::FsScope;
use tauri::{ use tauri::{
plugin::{Builder as PluginBuilder, TauriPlugin}, plugin::{Builder as PluginBuilder, TauriPlugin},

@ -1,4 +1,4 @@
![plugin-global-shortcut](banner.png) ![plugin-global-shortcut](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/global-shortcut/banner.png)
Register global shortcuts. Register global shortcuts.

@ -36,7 +36,7 @@ export type ShortcutHandler = (shortcut: string) => void;
*/ */
async function register( async function register(
shortcut: string, shortcut: string,
handler: ShortcutHandler handler: ShortcutHandler,
): Promise<void> { ): Promise<void> {
return await window.__TAURI_INVOKE__("plugin:globalShortcut|register", { return await window.__TAURI_INVOKE__("plugin:globalShortcut|register", {
shortcut, shortcut,
@ -61,7 +61,7 @@ async function register(
*/ */
async function registerAll( async function registerAll(
shortcuts: string[], shortcuts: string[],
handler: ShortcutHandler handler: ShortcutHandler,
): Promise<void> { ): Promise<void> {
return await window.__TAURI_INVOKE__("plugin:globalShortcut|register_all", { return await window.__TAURI_INVOKE__("plugin:globalShortcut|register_all", {
shortcuts, shortcuts,

@ -5,7 +5,7 @@ import { createConfig } from "../../shared/rollup.config.mjs";
export default createConfig({ export default createConfig({
input: "guest-js/index.ts", input: "guest-js/index.ts",
pkg: JSON.parse( pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8") readFileSync(new URL("./package.json", import.meta.url), "utf8"),
), ),
external: [/^@tauri-apps\/api/], external: [/^@tauri-apps\/api/],
}); });

@ -2,6 +2,16 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/global-shortcut/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/global-shortcut)
//!
//! Register global shortcuts.
//!
//! - Supported platforms: Windows, Linux and macOS.
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
#![cfg(not(any(target_os = "android", target_os = "ios")))] #![cfg(not(any(target_os = "android", target_os = "ios")))]
use std::{ use std::{

@ -13,14 +13,28 @@ tauri = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
tauri-plugin-fs = { path = "../fs", version = "2.0.0-alpha.0" } tauri-plugin-fs = { path = "../fs", version = "2.0.0-alpha.0" }
glob = "0.3" glob = "0.3"
rand = "0.8"
bytes = { version = "1", features = [ "serde" ] }
serde_repr = "0.1"
http = "0.2" http = "0.2"
reqwest = { version = "0.11", default-features = false, features = [ "json", "stream" ] } reqwest = { version = "0.11", default-features = false }
url = "2.4"
data-url = "0.3"
[features] [features]
multipart = [ "reqwest/multipart" ] multipart = ["reqwest/multipart"]
native-tls = [ "reqwest/native-tls" ] json = ["reqwest/json"]
native-tls-vendored = [ "reqwest/native-tls-vendored" ] stream = ["reqwest/stream"]
rustls-tls = [ "reqwest/rustls-tls" ] native-tls = ["reqwest/native-tls"]
native-tls-vendored = ["reqwest/native-tls-vendored"]
rustls-tls = ["reqwest/rustls-tls"]
default-tls = ["reqwest/default-tls"]
native-tls-alpn = ["reqwest/native-tls-alpn"]
rustls-tls-manual-roots = ["reqwest/rustls-tls-manual-roots"]
rustls-tls-webpki-roots = ["reqwest/rustls-tls-webpki-roots"]
rustls-tls-native-roots = ["reqwest/rustls-tls-native-roots"]
blocking = ["reqwest/blocking"]
cookies = ["reqwest/cookies"]
gzip = ["reqwest/gzip"]
brotli = ["reqwest/brotli"]
deflate = ["reqwest/deflate"]
trust-dns = ["reqwest/trust-dns"]
socks = ["reqwest/socks"]
http3 = ["reqwest/http3"]

@ -1,4 +1,4 @@
![plugin-http](banner.png) ![plugin-http](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/http/banner.png)
Access the HTTP client written in Rust. Access the HTTP client written in Rust.

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
/** /**
* Access the HTTP client written in Rust. * Make HTTP requests with the Rust backend.
* *
* ## Security * ## Security
* *
@ -31,518 +31,94 @@ declare global {
} }
/** /**
* Options to configure the Rust client used to make fetch requests
*
* @since 2.0.0 * @since 2.0.0
*/ */
interface Duration { export interface ClientOptions {
secs: number;
nanos: number;
}
/**
* @since 2.0.0
*/
interface ClientOptions {
/** /**
* Defines the maximum number of redirects the client should follow. * Defines the maximum number of redirects the client should follow.
* If set to 0, no redirects will be followed. * If set to 0, no redirects will be followed.
*/ */
maxRedirections?: number; maxRedirections?: number;
connectTimeout?: number | Duration; /** Timeout in milliseconds */
connectTimeout?: number;
} }
/** /**
* @since 2.0.0 * Fetch a resource from the network. It returns a `Promise` that resolves to the
*/ * `Response` to that `Request`, whether it is successful or not.
enum ResponseType {
JSON = 1,
Text = 2,
Binary = 3,
}
/**
* @since 2.0.0
*/
interface FilePart<T> {
file: string | T;
mime?: string;
fileName?: string;
}
type Part = string | Uint8Array | FilePart<Uint8Array>;
/**
* The body object to be used on POST and PUT requests.
* *
* @since 2.0.0 * @example
*/ * ```typescript
class Body { * const response = await fetch("http://my.json.host/data.json");
type: string; * console.log(response.status); // e.g. 200
payload: unknown; * console.log(response.statusText); // e.g. "OK"
* const jsonData = await response.json();
/** @ignore */ * ```
private constructor(type: string, payload: unknown) {
this.type = type;
this.payload = payload;
}
/**
* Creates a new form data body. The form data is an object where each key is the entry name,
* and the value is either a string or a file object.
*
* By default it sets the `application/x-www-form-urlencoded` Content-Type header,
* but you can set it to `multipart/form-data` if the Cargo feature `multipart` is enabled.
*
* Note that a file path must be allowed in the `fs` scope.
* @example
* ```typescript
* import { Body } from "@tauri-apps/plugin-http"
* const body = Body.form({
* key: 'value',
* image: {
* file: '/path/to/file', // either a path or an array buffer of the file contents
* mime: 'image/jpeg', // optional
* fileName: 'image.jpg' // optional
* }
* });
*
* // alternatively, use a FormData:
* const form = new FormData();
* form.append('key', 'value');
* form.append('image', file, 'image.png');
* const formBody = Body.form(form);
* ```
*
* @param data The body data.
*
* @returns The body object ready to be used on the POST and PUT requests.
*
* @since 2.0.0
*/
static form(data: Record<string, Part> | FormData): Body {
const form: Record<string, string | number[] | FilePart<number[]>> = {};
const append = (
key: string,
v: string | Uint8Array | FilePart<Uint8Array> | File
): void => {
if (v !== null) {
let r;
if (typeof v === "string") {
r = v;
} else if (v instanceof Uint8Array || Array.isArray(v)) {
r = Array.from(v);
} else if (v instanceof File) {
r = { file: v.name, mime: v.type, fileName: v.name };
} else if (typeof v.file === "string") {
r = { file: v.file, mime: v.mime, fileName: v.fileName };
} else {
r = { file: Array.from(v.file), mime: v.mime, fileName: v.fileName };
}
form[String(key)] = r;
}
};
if (data instanceof FormData) {
for (const [key, value] of data) {
append(key, value);
}
} else {
for (const [key, value] of Object.entries(data)) {
append(key, value);
}
}
return new Body("Form", form);
}
/**
* Creates a new JSON body.
* @example
* ```typescript
* import { Body } from "@tauri-apps/plugin-http"
* Body.json({
* registered: true,
* name: 'tauri'
* });
* ```
*
* @param data The body JSON object.
*
* @returns The body object ready to be used on the POST and PUT requests.
*
* @since 2.0.0
*/
static json<K extends string | number | symbol, V>(data: Record<K, V>): Body {
return new Body("Json", data);
}
/**
* Creates a new UTF-8 string body.
* @example
* ```typescript
* import { Body } from "@tauri-apps/plugin-http"
* Body.text('The body content as a string');
* ```
*
* @param value The body string.
*
* @returns The body object ready to be used on the POST and PUT requests.
*
* @since 2.0.0
*/
static text(value: string): Body {
return new Body("Text", value);
}
/**
* Creates a new byte array body.
* @example
* ```typescript
* import { Body } from "@tauri-apps/plugin-http"
* Body.bytes(new Uint8Array([1, 2, 3]));
* ```
*
* @param bytes The body byte array.
*
* @returns The body object ready to be used on the POST and PUT requests.
*
* @since 2.0.0
*/
static bytes(
bytes: Iterable<number> | ArrayLike<number> | ArrayBuffer
): Body {
// stringifying Uint8Array doesn't return an array of numbers, so we create one here
return new Body(
"Bytes",
Array.from(bytes instanceof ArrayBuffer ? new Uint8Array(bytes) : bytes)
);
}
}
/** The request HTTP verb. */
type HttpVerb =
| "GET"
| "POST"
| "PUT"
| "DELETE"
| "PATCH"
| "HEAD"
| "OPTIONS"
| "CONNECT"
| "TRACE";
/**
* Options object sent to the backend.
* *
* @since 2.0.0 * @since 2.0.0
*/ */
interface HttpOptions { export async function fetch(
method: HttpVerb; input: URL | Request | string,
url: string; init?: RequestInit & ClientOptions,
headers?: Record<string, unknown>; ): Promise<Response> {
query?: Record<string, unknown>; const maxRedirections = init?.maxRedirections;
body?: Body; const connectTimeout = init?.maxRedirections;
timeout?: number | Duration;
responseType?: ResponseType; // Remove these fields before creating the request
} if (init) {
delete init.maxRedirections;
/** Request options. */ delete init.connectTimeout;
type RequestOptions = Omit<HttpOptions, "method" | "url">; }
/** Options for the `fetch` API. */
type FetchOptions = Omit<HttpOptions, "url">; const req = new Request(input, init);
const buffer = await req.arrayBuffer();
/** @ignore */ const reqData = buffer.byteLength ? Array.from(new Uint8Array(buffer)) : null;
interface IResponse<T> {
url: string; const rid = await window.__TAURI_INVOKE__<number>("plugin:http|fetch", {
status: number; cmd: "fetch",
headers: Record<string, string>; method: req.method,
rawHeaders: Record<string, string[]>; url: req.url,
data: T; headers: Array.from(req.headers.entries()),
} data: reqData,
maxRedirections,
/** connectTimeout,
* Response object. });
*
* @since 2.0.0
* */
class Response<T> {
/** The request URL. */
url: string;
/** The response status code. */
status: number;
/** A boolean indicating whether the response was successful (status in the range 200299) or not. */
ok: boolean;
/** The response headers. */
headers: Record<string, string>;
/** The response raw headers. */
rawHeaders: Record<string, string[]>;
/** The response data. */
data: T;
/** @ignore */
constructor(response: IResponse<T>) {
this.url = response.url;
this.status = response.status;
this.ok = this.status >= 200 && this.status < 300;
this.headers = response.headers;
this.rawHeaders = response.rawHeaders;
this.data = response.data;
}
}
/**
* @since 2.0.0
*/
class Client {
id: number;
/** @ignore */
constructor(id: number) {
this.id = id;
}
/**
* Drops the client instance.
* @example
* ```typescript
* import { getClient } from '@tauri-apps/plugin-http';
* const client = await getClient();
* await client.drop();
* ```
*/
async drop(): Promise<void> {
return window.__TAURI_INVOKE__("plugin:http|drop_client", {
client: this.id,
});
}
/**
* Makes an HTTP request.
* @example
* ```typescript
* import { getClient } from '@tauri-apps/plugin-http';
* const client = await getClient();
* const response = await client.request({
* method: 'GET',
* url: 'http://localhost:3003/users',
* });
* ```
*/
async request<T>(options: HttpOptions): Promise<Response<T>> {
const jsonResponse =
!options.responseType || options.responseType === ResponseType.JSON;
if (jsonResponse) {
options.responseType = ResponseType.Text;
}
return window
.__TAURI_INVOKE__<IResponse<T>>("plugin:http|request", {
clientId: this.id,
options,
})
.then((res) => {
const response = new Response(res);
if (jsonResponse) {
/* eslint-disable */
try {
response.data = JSON.parse(response.data as string);
} catch (e) {
if (response.ok && (response.data as unknown as string) === "") {
response.data = {} as T;
} else if (response.ok) {
throw Error(
`Failed to parse response \`${response.data}\` as JSON: ${e};
try setting the \`responseType\` option to \`ResponseType.Text\` or \`ResponseType.Binary\` if the API does not return a JSON response.`
);
}
}
/* eslint-enable */
return response;
}
return response;
});
}
/**
* Makes a GET request.
* @example
* ```typescript
* import { getClient, ResponseType } from '@tauri-apps/plugin-http';
* const client = await getClient();
* const response = await client.get('http://localhost:3003/users', {
* timeout: 30,
* // the expected response type
* responseType: ResponseType.JSON
* });
* ```
*/
async get<T>(url: string, options?: RequestOptions): Promise<Response<T>> {
return this.request({
method: "GET",
url,
...options,
});
}
/**
* Makes a POST request.
* @example
* ```typescript
* import { getClient, Body, ResponseType } from '@tauri-apps/plugin-http';
* const client = await getClient();
* const response = await client.post('http://localhost:3003/users', {
* body: Body.json({
* name: 'tauri',
* password: 'awesome'
* }),
* // in this case the server returns a simple string
* responseType: ResponseType.Text,
* });
* ```
*/
async post<T>(
url: string,
body?: Body,
options?: RequestOptions
): Promise<Response<T>> {
return this.request({
method: "POST",
url,
body,
...options,
});
}
/** req.signal.addEventListener("abort", () => {
* Makes a PUT request. window.__TAURI_INVOKE__("plugin:http|fetch_cancel", {
* @example rid,
* ```typescript
* import { getClient, Body } from '@tauri-apps/plugin-http';
* const client = await getClient();
* const response = await client.put('http://localhost:3003/users/1', {
* body: Body.form({
* file: {
* file: '/home/tauri/avatar.png',
* mime: 'image/png',
* fileName: 'avatar.png'
* }
* })
* });
* ```
*/
async put<T>(
url: string,
body?: Body,
options?: RequestOptions
): Promise<Response<T>> {
return this.request({
method: "PUT",
url,
body,
...options,
}); });
} });
/** interface FetchSendResponse {
* Makes a PATCH request. status: number;
* @example statusText: string;
* ```typescript headers: [[string, string]];
* import { getClient, Body } from '@tauri-apps/plugin-http'; url: string;
* const client = await getClient();
* const response = await client.patch('http://localhost:3003/users/1', {
* body: Body.json({ email: 'contact@tauri.app' })
* });
* ```
*/
async patch<T>(url: string, options?: RequestOptions): Promise<Response<T>> {
return this.request({
method: "PATCH",
url,
...options,
});
} }
/** const { status, statusText, url, headers } =
* Makes a DELETE request. await window.__TAURI_INVOKE__<FetchSendResponse>("plugin:http|fetch_send", {
* @example rid,
* ```typescript
* import { getClient } from '@tauri-apps/plugin-http';
* const client = await getClient();
* const response = await client.delete('http://localhost:3003/users/1');
* ```
*/
async delete<T>(url: string, options?: RequestOptions): Promise<Response<T>> {
return this.request({
method: "DELETE",
url,
...options,
}); });
}
}
/**
* Creates a new client using the specified options.
* @example
* ```typescript
* import { getClient } from '@tauri-apps/plugin-http';
* const client = await getClient();
* ```
*
* @param options Client configuration.
*
* @returns A promise resolving to the client instance.
*
* @since 2.0.0
*/
async function getClient(options?: ClientOptions): Promise<Client> {
return window
.__TAURI_INVOKE__<number>("plugin:http|create_client", {
options,
})
.then((id) => new Client(id));
}
/** @internal */
let defaultClient: Client | null = null;
/** const body = await window.__TAURI_INVOKE__<number[]>(
* Perform an HTTP request using the default client. "plugin:http|fetch_read_body",
* @example {
* ```typescript rid,
* import { fetch } from '@tauri-apps/plugin-http'; },
* const response = await fetch('http://localhost:3003/users/2', { );
* method: 'GET',
* timeout: 30, const res = new Response(Uint8Array.from(body), {
* }); headers,
* ``` status,
*/ statusText,
async function fetch<T>(
url: string,
options?: FetchOptions
): Promise<Response<T>> {
if (defaultClient === null) {
defaultClient = await getClient();
}
return defaultClient.request({
url,
method: options?.method ?? "GET",
...options,
}); });
}
export type { // url is read only but seems like we can do this
Duration, Object.defineProperty(res, "url", { value: url });
ClientOptions,
Part,
HttpVerb,
HttpOptions,
RequestOptions,
FetchOptions,
};
export { return res;
getClient, }
fetch,
Body,
Client,
Response,
ResponseType,
type FilePart,
};

@ -5,7 +5,7 @@ import { createConfig } from "../../shared/rollup.config.mjs";
export default createConfig({ export default createConfig({
input: "guest-js/index.ts", input: "guest-js/index.ts",
pkg: JSON.parse( pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8") readFileSync(new URL("./package.json", import.meta.url), "utf8"),
), ),
external: [/^@tauri-apps\/api/], external: [/^@tauri-apps\/api/],
}); });

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_HTTP__=function(e){"use strict";var t;e.ResponseType=void 0,(t=e.ResponseType||(e.ResponseType={}))[t.JSON=1]="JSON",t[t.Text=2]="Text",t[t.Binary=3]="Binary";class r{constructor(e,t){this.type=e,this.payload=t}static form(e){const t={},s=(e,r)=>{if(null!==r){let s;s="string"==typeof r?r:r instanceof Uint8Array||Array.isArray(r)?Array.from(r):r instanceof File?{file:r.name,mime:r.type,fileName:r.name}:"string"==typeof r.file?{file:r.file,mime:r.mime,fileName:r.fileName}:{file:Array.from(r.file),mime:r.mime,fileName:r.fileName},t[String(e)]=s}};if(e instanceof FormData)for(const[t,r]of e)s(t,r);else for(const[t,r]of Object.entries(e))s(t,r);return new r("Form",t)}static json(e){return new r("Json",e)}static text(e){return new r("Text",e)}static bytes(e){return new r("Bytes",Array.from(e instanceof ArrayBuffer?new Uint8Array(e):e))}}class s{constructor(e){this.url=e.url,this.status=e.status,this.ok=this.status>=200&&this.status<300,this.headers=e.headers,this.rawHeaders=e.rawHeaders,this.data=e.data}}class n{constructor(e){this.id=e}async drop(){return window.__TAURI_INVOKE__("plugin:http|drop_client",{client:this.id})}async request(t){const r=!t.responseType||t.responseType===e.ResponseType.JSON;return r&&(t.responseType=e.ResponseType.Text),window.__TAURI_INVOKE__("plugin:http|request",{clientId:this.id,options:t}).then((e=>{const t=new s(e);if(r){try{t.data=JSON.parse(t.data)}catch(e){if(t.ok&&""===t.data)t.data={};else if(t.ok)throw Error(`Failed to parse response \`${t.data}\` as JSON: ${e};\n try setting the \`responseType\` option to \`ResponseType.Text\` or \`ResponseType.Binary\` if the API does not return a JSON response.`)}return t}return t}))}async get(e,t){return this.request({method:"GET",url:e,...t})}async post(e,t,r){return this.request({method:"POST",url:e,body:t,...r})}async put(e,t,r){return this.request({method:"PUT",url:e,body:t,...r})}async patch(e,t){return this.request({method:"PATCH",url:e,...t})}async delete(e,t){return this.request({method:"DELETE",url:e,...t})}}async function i(e){return window.__TAURI_INVOKE__("plugin:http|create_client",{options:e}).then((e=>new n(e)))}let o=null;return e.Body=r,e.Client=n,e.Response=s,e.fetch=async function(e,t){var r;return null===o&&(o=await i()),o.request({url:e,method:null!==(r=null==t?void 0:t.method)&&void 0!==r?r:"GET",...t})},e.getClient=i,e}({});Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_HTTP__})} if("__TAURI__"in window){var __TAURI_HTTP__=function(e){"use strict";return e.fetch=async function(e,t){const n=null==t?void 0:t.maxRedirections,r=null==t?void 0:t.maxRedirections;t&&(delete t.maxRedirections,delete t.connectTimeout);const _=new Request(e,t),i=await _.arrayBuffer(),a=i.byteLength?Array.from(new Uint8Array(i)):null,d=await window.__TAURI_INVOKE__("plugin:http|fetch",{cmd:"fetch",method:_.method,url:_.url,headers:Array.from(_.headers.entries()),data:a,maxRedirections:n,connectTimeout:r});_.signal.addEventListener("abort",(()=>{window.__TAURI_INVOKE__("plugin:http|fetch_cancel",{rid:d})}));const{status:o,statusText:s,url:c,headers:u}=await window.__TAURI_INVOKE__("plugin:http|fetch_send",{rid:d}),l=await window.__TAURI_INVOKE__("plugin:http|fetch_read_body",{rid:d}),w=new Response(Uint8Array.from(l),{headers:u,status:o,statusText:s});return Object.defineProperty(w,"url",{value:c}),w},e}({});Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_HTTP__})}

@ -0,0 +1,178 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{collections::HashMap, time::Duration};
use http::{header, HeaderName, HeaderValue, Method, StatusCode};
use reqwest::redirect::Policy;
use serde::Serialize;
use tauri::{command, AppHandle, Runtime};
use crate::{Error, FetchRequest, HttpExt, RequestId};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FetchResponse {
status: u16,
status_text: String,
headers: Vec<(String, String)>,
url: String,
}
#[command]
pub async fn fetch<R: Runtime>(
app: AppHandle<R>,
method: String,
url: url::Url,
headers: Vec<(String, String)>,
data: Option<Vec<u8>>,
connect_timeout: Option<u64>,
max_redirections: Option<usize>,
) -> crate::Result<RequestId> {
let scheme = url.scheme();
let method = Method::from_bytes(method.as_bytes())?;
let headers: HashMap<String, String> = HashMap::from_iter(headers);
match scheme {
"http" | "https" => {
if app.http().scope.is_allowed(&url) {
let mut builder = reqwest::ClientBuilder::new();
if let Some(timeout) = connect_timeout {
builder = builder.connect_timeout(Duration::from_millis(timeout));
}
if let Some(max_redirections) = max_redirections {
builder = builder.redirect(if max_redirections == 0 {
Policy::none()
} else {
Policy::limited(max_redirections)
});
}
let mut request = builder.build()?.request(method.clone(), url);
for (key, value) in &headers {
let name = HeaderName::from_bytes(key.as_bytes())?;
let v = HeaderValue::from_bytes(value.as_bytes())?;
if !matches!(name, header::HOST | header::CONTENT_LENGTH) {
request = request.header(name, v);
}
}
// POST and PUT requests should always have a 0 length content-length,
// if there is no body. https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
if data.is_none() && matches!(method, Method::POST | Method::PUT) {
request = request.header(header::CONTENT_LENGTH, HeaderValue::from(0));
}
if headers.contains_key(header::RANGE.as_str()) {
// https://fetch.spec.whatwg.org/#http-network-or-cache-fetch step 18
// If httpRequests header list contains `Range`, then append (`Accept-Encoding`, `identity`)
request = request.header(
header::ACCEPT_ENCODING,
HeaderValue::from_static("identity"),
);
}
if !headers.contains_key(header::USER_AGENT.as_str()) {
request = request.header(header::USER_AGENT, HeaderValue::from_static("tauri"));
}
if let Some(data) = data {
request = request.body(data);
}
let http_state = app.http();
let rid = http_state.next_id();
let fut = async move { Ok(request.send().await.map_err(Into::into)) };
let mut request_table = http_state.requests.lock().await;
request_table.insert(rid, FetchRequest::new(Box::pin(fut)));
Ok(rid)
} else {
Err(Error::UrlNotAllowed(url))
}
}
"data" => {
let data_url =
data_url::DataUrl::process(url.as_str()).map_err(|_| Error::DataUrlError)?;
let (body, _) = data_url
.decode_to_vec()
.map_err(|_| Error::DataUrlDecodeError)?;
let response = http::Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, data_url.mime_type().to_string())
.body(reqwest::Body::from(body))?;
let http_state = app.http();
let rid = http_state.next_id();
let fut = async move { Ok(Ok(reqwest::Response::from(response))) };
let mut request_table = http_state.requests.lock().await;
request_table.insert(rid, FetchRequest::new(Box::pin(fut)));
Ok(rid)
}
_ => Err(Error::SchemeNotSupport(scheme.to_string())),
}
}
#[command]
pub async fn fetch_cancel<R: Runtime>(app: AppHandle<R>, rid: RequestId) -> crate::Result<()> {
let mut request_table = app.http().requests.lock().await;
let req = request_table
.get_mut(&rid)
.ok_or(Error::InvalidRequestId(rid))?;
*req = FetchRequest::new(Box::pin(async { Err(Error::RequestCanceled) }));
Ok(())
}
#[command]
pub async fn fetch_send<R: Runtime>(
app: AppHandle<R>,
rid: RequestId,
) -> crate::Result<FetchResponse> {
let mut request_table = app.http().requests.lock().await;
let req = request_table
.remove(&rid)
.ok_or(Error::InvalidRequestId(rid))?;
let res = match req.0.lock().await.as_mut().await {
Ok(Ok(res)) => res,
Ok(Err(e)) | Err(e) => return Err(e),
};
let status = res.status();
let url = res.url().to_string();
let mut headers = Vec::new();
for (key, val) in res.headers().iter() {
headers.push((
key.as_str().into(),
String::from_utf8(val.as_bytes().to_vec())?,
));
}
app.http().responses.lock().await.insert(rid, res);
Ok(FetchResponse {
status: status.as_u16(),
status_text: status.canonical_reason().unwrap_or_default().to_string(),
headers,
url,
})
}
// TODO: change return value to tauri::ipc::Response on next alpha
#[command]
pub(crate) async fn fetch_read_body<R: Runtime>(
app: AppHandle<R>,
rid: RequestId,
) -> crate::Result<Vec<u8>> {
let mut response_table = app.http().responses.lock().await;
let res = response_table
.remove(&rid)
.ok_or(Error::InvalidRequestId(rid))?;
Ok(res.bytes().await?.to_vec())
}

@ -1,341 +0,0 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{collections::HashMap, path::PathBuf, time::Duration};
use reqwest::{header, Method, Url};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use serde_repr::{Deserialize_repr, Serialize_repr};
#[derive(Deserialize)]
#[serde(untagged)]
enum SerdeDuration {
Seconds(u64),
Duration(Duration),
}
fn deserialize_duration<'de, D: Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Option<Duration>, D::Error> {
if let Some(duration) = Option::<SerdeDuration>::deserialize(deserializer)? {
Ok(Some(match duration {
SerdeDuration::Seconds(s) => Duration::from_secs(s),
SerdeDuration::Duration(d) => d,
}))
} else {
Ok(None)
}
}
/// The builder of [`Client`].
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientBuilder {
/// Max number of redirections to follow.
pub max_redirections: Option<usize>,
/// Connect timeout for the request.
#[serde(deserialize_with = "deserialize_duration", default)]
pub connect_timeout: Option<Duration>,
}
impl ClientBuilder {
/// Builds the Client.
pub fn build(self) -> crate::Result<Client> {
let mut client_builder = reqwest::Client::builder();
if let Some(max_redirections) = self.max_redirections {
client_builder = client_builder.redirect(if max_redirections == 0 {
reqwest::redirect::Policy::none()
} else {
reqwest::redirect::Policy::limited(max_redirections)
});
}
if let Some(connect_timeout) = self.connect_timeout {
client_builder = client_builder.connect_timeout(connect_timeout);
}
let client = client_builder.build()?;
Ok(Client(client))
}
}
/// The HTTP client based on [`reqwest`].
#[derive(Debug, Clone)]
pub struct Client(reqwest::Client);
impl Client {
/// Executes an HTTP request
///
/// # Examples
pub async fn send(&self, mut request: HttpRequestBuilder) -> crate::Result<Response> {
let method = Method::from_bytes(request.method.to_uppercase().as_bytes())?;
let mut request_builder = self.0.request(method, request.url.as_str());
if let Some(query) = request.query {
request_builder = request_builder.query(&query);
}
if let Some(timeout) = request.timeout {
request_builder = request_builder.timeout(timeout);
}
if let Some(body) = request.body {
request_builder = match body {
Body::Bytes(data) => request_builder.body(bytes::Bytes::from(data)),
Body::Text(text) => request_builder.body(bytes::Bytes::from(text)),
Body::Json(json) => request_builder.json(&json),
Body::Form(form_body) => {
#[allow(unused_variables)]
fn send_form(
request_builder: reqwest::RequestBuilder,
headers: &mut Option<HeaderMap>,
form_body: FormBody,
) -> crate::Result<reqwest::RequestBuilder> {
#[cfg(feature = "multipart")]
if matches!(
headers
.as_ref()
.and_then(|h| h.0.get("content-type"))
.map(|v| v.as_bytes()),
Some(b"multipart/form-data")
) {
// the Content-Type header will be set by reqwest in the `.multipart` call
headers.as_mut().map(|h| h.0.remove("content-type"));
let mut multipart = reqwest::multipart::Form::new();
for (name, part) in form_body.0 {
let part = match part {
FormPart::File {
file,
mime,
file_name,
} => {
let bytes: Vec<u8> = file.try_into()?;
let mut part = reqwest::multipart::Part::bytes(bytes);
if let Some(mime) = mime {
part = part.mime_str(&mime)?;
}
if let Some(file_name) = file_name {
part = part.file_name(file_name);
}
part
}
FormPart::Text(value) => reqwest::multipart::Part::text(value),
};
multipart = multipart.part(name, part);
}
return Ok(request_builder.multipart(multipart));
}
let mut form = Vec::new();
for (name, part) in form_body.0 {
match part {
FormPart::File { file, .. } => {
let bytes: Vec<u8> = file.try_into()?;
form.push((name, serde_json::to_string(&bytes)?))
}
FormPart::Text(value) => form.push((name, value)),
}
}
Ok(request_builder.form(&form))
}
send_form(request_builder, &mut request.headers, form_body)?
}
};
}
if let Some(headers) = request.headers {
request_builder = request_builder.headers(headers.0);
}
let http_request = request_builder.build()?;
let response = self.0.execute(http_request).await?;
Ok(Response(
request.response_type.unwrap_or(ResponseType::Json),
response,
))
}
}
#[derive(Serialize_repr, Deserialize_repr, Clone, Debug)]
#[repr(u16)]
#[non_exhaustive]
/// The HTTP response type.
pub enum ResponseType {
/// Read the response as JSON
Json = 1,
/// Read the response as text
Text,
/// Read the response as binary
Binary,
}
#[derive(Debug)]
pub struct Response(ResponseType, reqwest::Response);
impl Response {
/// Reads the response.
///
/// Note that the body is serialized to a [`Value`].
pub async fn read(self) -> crate::Result<ResponseData> {
let url = self.1.url().clone();
let mut headers = HashMap::new();
let mut raw_headers = HashMap::new();
for (name, value) in self.1.headers() {
headers.insert(
name.as_str().to_string(),
String::from_utf8(value.as_bytes().to_vec())?,
);
raw_headers.insert(
name.as_str().to_string(),
self.1
.headers()
.get_all(name)
.into_iter()
.map(|v| String::from_utf8(v.as_bytes().to_vec()).map_err(Into::into))
.collect::<crate::Result<Vec<String>>>()?,
);
}
let status = self.1.status().as_u16();
let data = match self.0 {
ResponseType::Json => self.1.json().await?,
ResponseType::Text => Value::String(self.1.text().await?),
ResponseType::Binary => serde_json::to_value(&self.1.bytes().await?)?,
};
Ok(ResponseData {
url,
status,
headers,
raw_headers,
data,
})
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ResponseData {
/// Response URL. Useful if it followed redirects.
pub url: Url,
/// Response status code.
pub status: u16,
/// Response headers.
pub headers: HashMap<String, String>,
/// Response raw headers.
pub raw_headers: HashMap<String, Vec<String>>,
/// Response data.
pub data: Value,
}
/// A file path or contents.
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum FilePart {
/// File path.
Path(PathBuf),
/// File contents.
Contents(Vec<u8>),
}
impl TryFrom<FilePart> for Vec<u8> {
type Error = crate::Error;
fn try_from(file: FilePart) -> crate::Result<Self> {
let bytes = match file {
FilePart::Path(path) => std::fs::read(path)?,
FilePart::Contents(bytes) => bytes,
};
Ok(bytes)
}
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum FormPart {
/// A string value.
Text(String),
/// A file value.
#[serde(rename_all = "camelCase")]
File {
/// File path or content.
file: FilePart,
/// Mime type of this part.
/// Only used when the `Content-Type` header is set to `multipart/form-data`.
mime: Option<String>,
/// File name.
/// Only used when the `Content-Type` header is set to `multipart/form-data`.
file_name: Option<String>,
},
}
#[derive(Debug, Deserialize)]
pub struct FormBody(pub(crate) HashMap<String, FormPart>);
#[derive(Debug, Deserialize)]
#[serde(tag = "type", content = "payload")]
#[non_exhaustive]
pub enum Body {
Form(FormBody),
Json(Value),
Text(String),
Bytes(Vec<u8>),
}
#[derive(Debug, Default)]
pub struct HeaderMap(header::HeaderMap);
impl<'de> Deserialize<'de> for HeaderMap {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let map = HashMap::<String, String>::deserialize(deserializer)?;
let mut headers = header::HeaderMap::default();
for (key, value) in map {
if let (Ok(key), Ok(value)) = (
header::HeaderName::from_bytes(key.as_bytes()),
header::HeaderValue::from_str(&value),
) {
headers.insert(key, value);
} else {
return Err(serde::de::Error::custom(format!(
"invalid header `{key}` `{value}`"
)));
}
}
Ok(Self(headers))
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HttpRequestBuilder {
/// The request method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT or TRACE)
pub method: String,
/// The request URL
pub url: Url,
/// The request query params
pub query: Option<HashMap<String, String>>,
/// The request headers
pub headers: Option<HeaderMap>,
/// The request body
pub body: Option<Body>,
/// Timeout for the whole request
#[serde(deserialize_with = "deserialize_duration", default)]
pub timeout: Option<Duration>,
/// The response type (defaults to Json)
pub response_type: Option<ResponseType>,
}

@ -1,78 +0,0 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use tauri::{path::SafePathBuf, AppHandle, Runtime, State};
use tauri_plugin_fs::FsExt;
use crate::{ClientId, Http};
mod client;
use client::{Body, ClientBuilder, FilePart, FormPart, HttpRequestBuilder, ResponseData};
pub use client::Client;
#[tauri::command]
pub async fn create_client<R: Runtime>(
_app: AppHandle<R>,
http: State<'_, Http<R>>,
options: Option<ClientBuilder>,
) -> super::Result<ClientId> {
let client = options.unwrap_or_default().build()?;
let mut store = http.clients.lock().unwrap();
let id = rand::random::<ClientId>();
store.insert(id, client);
Ok(id)
}
#[tauri::command]
pub async fn drop_client<R: Runtime>(
_app: AppHandle<R>,
http: State<'_, Http<R>>,
client: ClientId,
) -> super::Result<()> {
let mut store = http.clients.lock().unwrap();
store.remove(&client);
Ok(())
}
#[tauri::command]
pub async fn request<R: Runtime>(
app: AppHandle<R>,
http: State<'_, Http<R>>,
client_id: ClientId,
options: Box<HttpRequestBuilder>,
) -> super::Result<ResponseData> {
if http.scope.is_allowed(&options.url) {
let client = http
.clients
.lock()
.unwrap()
.get(&client_id)
.ok_or_else(|| crate::Error::HttpClientNotInitialized)?
.clone();
let options = *options;
if let Some(Body::Form(form)) = &options.body {
for value in form.0.values() {
if let FormPart::File {
file: FilePart::Path(path),
..
} = value
{
if SafePathBuf::new(path.clone()).is_err()
|| !app
.try_fs_scope()
.map(|s| s.is_allowed(path))
.unwrap_or_default()
{
return Err(crate::Error::PathNotAllowed(path.clone()));
}
}
}
}
let response = client.send(options).await?;
Ok(response.read().await?)
} else {
Err(crate::Error::UrlNotAllowed(options.url))
}
}

@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use reqwest::Url;
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
@ -15,9 +14,9 @@ pub struct Config {
/// The scoped URL is matched against the request URL using a glob pattern. /// The scoped URL is matched against the request URL using a glob pattern.
/// ///
/// Examples: /// Examples:
/// - "https://**": allows all HTTPS urls /// - "https://*" or "https://**" : allows all HTTPS urls
/// - "https://*.github.com/tauri-apps/tauri": allows any subdomain of "github.com" with the "tauri-apps/api" path /// - "https://*.github.com/tauri-apps/tauri": allows any subdomain of "github.com" with the "tauri-apps/api" path
/// - "https://myapi.service.com/users/*": allows access to any URLs that begins with "https://myapi.service.com/users/" /// - "https://myapi.service.com/users/*": allows access to any URLs that begins with "https://myapi.service.com/users/"
#[allow(rustdoc::bare_urls)] #[allow(rustdoc::bare_urls)]
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)] #[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)]
pub struct HttpAllowlistScope(pub Vec<Url>); pub struct HttpAllowlistScope(pub Vec<String>);

@ -2,10 +2,10 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use std::path::PathBuf;
use reqwest::Url;
use serde::{Serialize, Serializer}; use serde::{Serialize, Serializer};
use url::Url;
use crate::RequestId;
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
@ -15,19 +15,32 @@ pub enum Error {
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error(transparent)] #[error(transparent)]
Network(#[from] reqwest::Error), Network(#[from] reqwest::Error),
#[error(transparent)]
Http(#[from] http::Error),
#[error(transparent)]
HttpInvalidHeaderName(#[from] http::header::InvalidHeaderName),
#[error(transparent)]
HttpInvalidHeaderValue(#[from] http::header::InvalidHeaderValue),
/// URL not allowed by the scope. /// URL not allowed by the scope.
#[error("url not allowed on the configured scope: {0}")] #[error("url not allowed on the configured scope: {0}")]
UrlNotAllowed(Url), UrlNotAllowed(Url),
/// Path not allowed by the scope. #[error(transparent)]
#[error("path not allowed on the configured scope: {0}")] UrlParseError(#[from] url::ParseError),
PathNotAllowed(PathBuf),
/// Client with specified ID not found.
#[error("http client dropped or not initialized")]
HttpClientNotInitialized,
/// HTTP method error. /// HTTP method error.
#[error(transparent)] #[error(transparent)]
HttpMethod(#[from] http::method::InvalidMethod), HttpMethod(#[from] http::method::InvalidMethod),
/// Failed to serialize header value as string. #[error("scheme {0} not supported")]
SchemeNotSupport(String),
#[error("Request canceled")]
RequestCanceled,
#[error(transparent)]
FsError(#[from] tauri_plugin_fs::Error),
#[error("failed to process data url")]
DataUrlError,
#[error("failed to decode data url into bytes")]
DataUrlDecodeError,
#[error("invalid request id: {0}")]
InvalidRequestId(RequestId),
#[error(transparent)] #[error(transparent)]
Utf8(#[from] std::string::FromUtf8Error), Utf8(#[from] std::string::FromUtf8Error),
} }
@ -40,3 +53,5 @@ impl Serialize for Error {
serializer.serialize_str(self.to_string().as_ref()) serializer.serialize_str(self.to_string().as_ref())
} }
} }
pub type Result<T> = std::result::Result<T, Error>;

@ -2,34 +2,60 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use config::{Config, HttpAllowlistScope}; //! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/http/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/http)
pub use reqwest as client; //!
//! Access the HTTP client written in Rust.
use std::sync::atomic::AtomicU32;
use std::{collections::HashMap, future::Future, pin::Pin};
pub use reqwest;
use reqwest::Response;
use tauri::async_runtime::Mutex;
use tauri::{ use tauri::{
plugin::{Builder, TauriPlugin}, plugin::{Builder, TauriPlugin},
AppHandle, Manager, Runtime, AppHandle, Manager, Runtime,
}; };
use std::{collections::HashMap, sync::Mutex}; use crate::config::{Config, HttpAllowlistScope};
pub use error::{Error, Result};
mod commands; mod commands;
mod config; mod config;
mod error; mod error;
mod scope; mod scope;
pub use error::Error; type RequestId = u32;
type Result<T> = std::result::Result<T, Error>; type CancelableResponseResult = Result<Result<reqwest::Response>>;
type ClientId = u32; type CancelableResponseFuture =
Pin<Box<dyn Future<Output = CancelableResponseResult> + Send + Sync>>;
type RequestTable = HashMap<RequestId, FetchRequest>;
type ResponseTable = HashMap<RequestId, Response>;
pub struct Http<R: Runtime> { struct FetchRequest(Mutex<CancelableResponseFuture>);
impl FetchRequest {
fn new(f: CancelableResponseFuture) -> Self {
Self(Mutex::new(f))
}
}
struct Http<R: Runtime> {
#[allow(dead_code)] #[allow(dead_code)]
app: AppHandle<R>, app: AppHandle<R>,
pub(crate) clients: Mutex<HashMap<ClientId, commands::Client>>, scope: scope::Scope,
pub(crate) scope: scope::Scope, current_id: AtomicU32,
requests: Mutex<RequestTable>,
responses: Mutex<ResponseTable>,
} }
impl<R: Runtime> Http<R> {} impl<R: Runtime> Http<R> {
fn next_id(&self) -> RequestId {
self.current_id
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
}
pub trait HttpExt<R: Runtime> { trait HttpExt<R: Runtime> {
fn http(&self) -> &Http<R>; fn http(&self) -> &Http<R>;
} }
@ -43,15 +69,18 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<Config>> {
Builder::<R, Option<Config>>::new("http") Builder::<R, Option<Config>>::new("http")
.js_init_script(include_str!("api-iife.js").to_string()) .js_init_script(include_str!("api-iife.js").to_string())
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
commands::create_client, commands::fetch,
commands::drop_client, commands::fetch_cancel,
commands::request commands::fetch_send,
commands::fetch_read_body,
]) ])
.setup(|app, api| { .setup(|app, api| {
let default_scope = HttpAllowlistScope::default(); let default_scope = HttpAllowlistScope::default();
app.manage(Http { app.manage(Http {
app: app.clone(), app: app.clone(),
clients: Default::default(), current_id: 0.into(),
requests: Default::default(),
responses: Default::default(),
scope: scope::Scope::new( scope: scope::Scope::new(
api.config() api.config()
.as_ref() .as_ref()

@ -2,10 +2,11 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use crate::config::HttpAllowlistScope;
use glob::Pattern; use glob::Pattern;
use reqwest::Url; use reqwest::Url;
use crate::config::HttpAllowlistScope;
/// Scope for filesystem access. /// Scope for filesystem access.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Scope { pub struct Scope {
@ -20,7 +21,7 @@ impl Scope {
.0 .0
.iter() .iter()
.map(|url| { .map(|url| {
glob::Pattern::new(url.as_str()).unwrap_or_else(|_| { glob::Pattern::new(url).unwrap_or_else(|_| {
panic!("scoped URL is not a valid glob pattern: `{url}`") panic!("scoped URL is not a valid glob pattern: `{url}`")
}) })
}) })
@ -30,9 +31,10 @@ impl Scope {
/// Determines if the given URL is allowed on this scope. /// Determines if the given URL is allowed on this scope.
pub fn is_allowed(&self, url: &Url) -> bool { pub fn is_allowed(&self, url: &Url) -> bool {
self.allowed_urls self.allowed_urls.iter().any(|allowed| {
.iter() allowed.matches(url.as_str())
.any(|allowed| allowed.matches(url.as_str())) || allowed.matches(url.as_str().strip_suffix('/').unwrap_or_default())
})
} }
} }
@ -79,7 +81,7 @@ mod tests {
let scope = super::Scope::new(&HttpAllowlistScope(vec!["http://*".parse().unwrap()])); let scope = super::Scope::new(&HttpAllowlistScope(vec!["http://*".parse().unwrap()]));
assert!(scope.is_allowed(&"http://something.else".parse().unwrap())); assert!(scope.is_allowed(&"http://something.else".parse().unwrap()));
assert!(!scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap())); assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap()));
assert!(!scope.is_allowed(&"https://something.else".parse().unwrap())); assert!(!scope.is_allowed(&"https://something.else".parse().unwrap()));
let scope = super::Scope::new(&HttpAllowlistScope(vec!["http://**".parse().unwrap()])); let scope = super::Scope::new(&HttpAllowlistScope(vec!["http://**".parse().unwrap()]));

@ -1,4 +1,4 @@
![plugin-localhost](banner.png) ![plugin-localhost](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/localhost/banner.png)
Expose your apps assets through a localhost server instead of the default custom protocol. Expose your apps assets through a localhost server instead of the default custom protocol.

@ -2,6 +2,17 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/localhost/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/localhost)
//!
//! Expose your apps assets through a localhost server instead of the default custom protocol.
//!
//! **Note: This plugins brings considerable security risks and you should only use it if you know what your are doing. If in doubt, use the default custom protocol implementation.**
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
use std::collections::HashMap; use std::collections::HashMap;
use http::Uri; use http::Uri;

@ -1,4 +1,4 @@
![plugin-log](banner.png) ![plugin-log](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/log/banner.png)
Configurable logging for your Tauri app. Configurable logging for your Tauri app.

@ -52,7 +52,7 @@ enum LogLevel {
async function log( async function log(
level: LogLevel, level: LogLevel,
message: string, message: string,
options?: LogOptions options?: LogOptions,
): Promise<void> { ): Promise<void> {
const traces = new Error().stack?.split("\n").map((line) => line.split("@")); const traces = new Error().stack?.split("\n").map((line) => line.split("@"));
@ -95,7 +95,7 @@ async function log(
*/ */
export async function error( export async function error(
message: string, message: string,
options?: LogOptions options?: LogOptions,
): Promise<void> { ): Promise<void> {
await log(LogLevel.Error, message, options); await log(LogLevel.Error, message, options);
} }
@ -117,7 +117,7 @@ export async function error(
*/ */
export async function warn( export async function warn(
message: string, message: string,
options?: LogOptions options?: LogOptions,
): Promise<void> { ): Promise<void> {
await log(LogLevel.Warn, message, options); await log(LogLevel.Warn, message, options);
} }
@ -139,7 +139,7 @@ export async function warn(
*/ */
export async function info( export async function info(
message: string, message: string,
options?: LogOptions options?: LogOptions,
): Promise<void> { ): Promise<void> {
await log(LogLevel.Info, message, options); await log(LogLevel.Info, message, options);
} }
@ -161,7 +161,7 @@ export async function info(
*/ */
export async function debug( export async function debug(
message: string, message: string,
options?: LogOptions options?: LogOptions,
): Promise<void> { ): Promise<void> {
await log(LogLevel.Debug, message, options); await log(LogLevel.Debug, message, options);
} }
@ -183,7 +183,7 @@ export async function debug(
*/ */
export async function trace( export async function trace(
message: string, message: string,
options?: LogOptions options?: LogOptions,
): Promise<void> { ): Promise<void> {
await log(LogLevel.Trace, message, options); await log(LogLevel.Trace, message, options);
} }
@ -202,7 +202,7 @@ export async function attachConsole(): Promise<UnlistenFn> {
// TODO: Investigate security/detect-unsafe-regex // TODO: Investigate security/detect-unsafe-regex
// eslint-disable-next-line no-control-regex, security/detect-unsafe-regex // eslint-disable-next-line no-control-regex, security/detect-unsafe-regex
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
"" "",
); );
switch (payload.level) { switch (payload.level) {

@ -25,7 +25,7 @@
"LICENSE" "LICENSE"
], ],
"devDependencies": { "devDependencies": {
"tslib": "^2.5.0" "tslib": "2.6.0"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.5" "@tauri-apps/api": "2.0.0-alpha.5"

@ -5,7 +5,7 @@ import { createConfig } from "../../shared/rollup.config.mjs";
export default createConfig({ export default createConfig({
input: "guest-js/index.ts", input: "guest-js/index.ts",
pkg: JSON.parse( pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8") readFileSync(new URL("./package.json", import.meta.url), "utf8"),
), ),
external: [/^@tauri-apps\/api/], external: [/^@tauri-apps\/api/],
}); });

@ -2,8 +2,15 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/log/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/log)
//!
//! Logging for Tauri applications. //! Logging for Tauri applications.
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
use fern::{Filter, FormatCallback}; use fern::{Filter, FormatCallback};
use log::{logger, RecordBuilder}; use log::{logger, RecordBuilder};
use log::{LevelFilter, Record}; use log::{LevelFilter, Record};

@ -1,4 +1,4 @@
![plugin-notification](banner.png) ![plugin-notification](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/notification/banner.png)
Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API. Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API.

@ -296,7 +296,7 @@ type Permission = "granted" | "denied" | "default";
* Checks if the permission to send notifications is granted. * Checks if the permission to send notifications is granted.
* @example * @example
* ```typescript * ```typescript
* import { isPermissionGranted } from '@tauri-apps/api/notification'; * import { isPermissionGranted } from '@tauri-apps/plugin-notification';
* const permissionGranted = await isPermissionGranted(); * const permissionGranted = await isPermissionGranted();
* ``` * ```
* *
@ -313,7 +313,7 @@ async function isPermissionGranted(): Promise<boolean> {
* Requests the permission to send notifications. * Requests the permission to send notifications.
* @example * @example
* ```typescript * ```typescript
* import { isPermissionGranted, requestPermission } from '@tauri-apps/api/notification'; * import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification';
* let permissionGranted = await isPermissionGranted(); * let permissionGranted = await isPermissionGranted();
* if (!permissionGranted) { * if (!permissionGranted) {
* const permission = await requestPermission(); * const permission = await requestPermission();
@ -333,7 +333,7 @@ async function requestPermission(): Promise<Permission> {
* Sends a notification to the user. * Sends a notification to the user.
* @example * @example
* ```typescript * ```typescript
* import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/api/notification'; * import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/plugin-notification';
* let permissionGranted = await isPermissionGranted(); * let permissionGranted = await isPermissionGranted();
* if (!permissionGranted) { * if (!permissionGranted) {
* const permission = await requestPermission(); * const permission = await requestPermission();
@ -362,7 +362,7 @@ function sendNotification(options: Options | string): void {
* *
* @example * @example
* ```typescript * ```typescript
* import { registerActionTypes } from '@tauri-apps/api/notification'; * import { registerActionTypes } from '@tauri-apps/plugin-notification';
* await registerActionTypes([{ * await registerActionTypes([{
* id: 'tauri', * id: 'tauri',
* actions: [{ * actions: [{
@ -385,7 +385,7 @@ async function registerActionTypes(types: ActionType[]): Promise<void> {
* *
* @example * @example
* ```typescript * ```typescript
* import { pending } from '@tauri-apps/api/notification'; * import { pending } from '@tauri-apps/plugin-notification';
* const pendingNotifications = await pending(); * const pendingNotifications = await pending();
* ``` * ```
* *
@ -402,7 +402,7 @@ async function pending(): Promise<PendingNotification[]> {
* *
* @example * @example
* ```typescript * ```typescript
* import { cancel } from '@tauri-apps/api/notification'; * import { cancel } from '@tauri-apps/plugin-notification';
* await cancel([-34234, 23432, 4311]); * await cancel([-34234, 23432, 4311]);
* ``` * ```
* *
@ -419,7 +419,7 @@ async function cancel(notifications: number[]): Promise<void> {
* *
* @example * @example
* ```typescript * ```typescript
* import { cancelAll } from '@tauri-apps/api/notification'; * import { cancelAll } from '@tauri-apps/plugin-notification';
* await cancelAll(); * await cancelAll();
* ``` * ```
* *
@ -436,7 +436,7 @@ async function cancelAll(): Promise<void> {
* *
* @example * @example
* ```typescript * ```typescript
* import { active } from '@tauri-apps/api/notification'; * import { active } from '@tauri-apps/plugin-notification';
* const activeNotifications = await active(); * const activeNotifications = await active();
* ``` * ```
* *
@ -453,7 +453,7 @@ async function active(): Promise<ActiveNotification[]> {
* *
* @example * @example
* ```typescript * ```typescript
* import { cancel } from '@tauri-apps/api/notification'; * import { cancel } from '@tauri-apps/plugin-notification';
* await cancel([-34234, 23432, 4311]) * await cancel([-34234, 23432, 4311])
* ``` * ```
* *
@ -470,7 +470,7 @@ async function removeActive(notifications: number[]): Promise<void> {
* *
* @example * @example
* ```typescript * ```typescript
* import { removeAllActive } from '@tauri-apps/api/notification'; * import { removeAllActive } from '@tauri-apps/plugin-notification';
* await removeAllActive() * await removeAllActive()
* ``` * ```
* *
@ -487,7 +487,7 @@ async function removeAllActive(): Promise<void> {
* *
* @example * @example
* ```typescript * ```typescript
* import { createChannel, Importance, Visibility } from '@tauri-apps/api/notification'; * import { createChannel, Importance, Visibility } from '@tauri-apps/plugin-notification';
* await createChannel({ * await createChannel({
* id: 'new-messages', * id: 'new-messages',
* name: 'New Messages', * name: 'New Messages',
@ -511,7 +511,7 @@ async function createChannel(channel: Channel): Promise<void> {
* *
* @example * @example
* ```typescript * ```typescript
* import { removeChannel } from '@tauri-apps/api/notification'; * import { removeChannel } from '@tauri-apps/plugin-notification';
* await removeChannel(); * await removeChannel();
* ``` * ```
* *
@ -528,7 +528,7 @@ async function removeChannel(id: string): Promise<void> {
* *
* @example * @example
* ```typescript * ```typescript
* import { channels } from '@tauri-apps/api/notification'; * import { channels } from '@tauri-apps/plugin-notification';
* const notificationChannels = await channels(); * const notificationChannels = await channels();
* ``` * ```
* *
@ -541,13 +541,13 @@ async function channels(): Promise<Channel[]> {
} }
async function onNotificationReceived( async function onNotificationReceived(
cb: (notification: Options) => void cb: (notification: Options) => void,
): Promise<PluginListener> { ): Promise<PluginListener> {
return addPluginListener("notification", "notification", cb); return addPluginListener("notification", "notification", cb);
} }
async function onAction( async function onAction(
cb: (notification: Options) => void cb: (notification: Options) => void,
): Promise<PluginListener> { ): Promise<PluginListener> {
return addPluginListener("notification", "actionPerformed", cb); return addPluginListener("notification", "actionPerformed", cb);
} }

@ -5,7 +5,7 @@ import { createConfig } from "../../shared/rollup.config.mjs";
export default createConfig({ export default createConfig({
input: "guest-js/index.ts", input: "guest-js/index.ts",
pkg: JSON.parse( pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8") readFileSync(new URL("./package.json", import.meta.url), "utf8"),
), ),
external: [/^@tauri-apps\/api/], external: [/^@tauri-apps\/api/],
}); });

@ -2,6 +2,15 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/notification/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/notification)
//!
//! Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API.
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
use serde::Serialize; use serde::Serialize;
#[cfg(mobile)] #[cfg(mobile)]
use tauri::plugin::PluginHandle; use tauri::plugin::PluginHandle;

@ -1,4 +1,4 @@
![plugin-os](banner.png) ![plugin-os](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/os/banner.png)
Read information about the operating system. Read information about the operating system.

@ -166,7 +166,7 @@ async function exeExtension(): Promise<string | null> {
* Returns the host name of the operating system. * Returns the host name of the operating system.
* @example * @example
* ```typescript * ```typescript
* import { hostname } from '@tauri-apps/api/os'; * import { hostname } from '@tauri-apps/plugin-os';
* const hostname = await hostname(); * const hostname = await hostname();
* ``` * ```
*/ */

@ -24,7 +24,7 @@
"LICENSE" "LICENSE"
], ],
"devDependencies": { "devDependencies": {
"tslib": "^2.5.0" "tslib": "2.6.0"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.5" "@tauri-apps/api": "2.0.0-alpha.5"

@ -5,7 +5,7 @@ import { createConfig } from "../../shared/rollup.config.mjs";
export default createConfig({ export default createConfig({
input: "guest-js/index.ts", input: "guest-js/index.ts",
pkg: JSON.parse( pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8") readFileSync(new URL("./package.json", import.meta.url), "utf8"),
), ),
external: [/^@tauri-apps\/api/], external: [/^@tauri-apps\/api/],
}); });

@ -2,6 +2,15 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/os/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/os)
//!
//! Read information about the operating system.
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
use std::fmt::Display; use std::fmt::Display;
pub use os_info::Version; pub use os_info::Version;

@ -5,6 +5,16 @@
- [`ebb2eb2`](https://github.com/tauri-apps/plugins-workspace/commit/ebb2eb2fe2ebfbb70530d16a983d396aa5829aa1)([#274](https://github.com/tauri-apps/plugins-workspace/pull/274)) Recursively unescape saved patterns before allowing/forbidding them. This effectively prevents `.persisted-scope` files from blowing up, which caused Out-Of-Memory issues, while automatically fixing existing broken files seamlessly. - [`ebb2eb2`](https://github.com/tauri-apps/plugins-workspace/commit/ebb2eb2fe2ebfbb70530d16a983d396aa5829aa1)([#274](https://github.com/tauri-apps/plugins-workspace/pull/274)) Recursively unescape saved patterns before allowing/forbidding them. This effectively prevents `.persisted-scope` files from blowing up, which caused Out-Of-Memory issues, while automatically fixing existing broken files seamlessly.
- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! - [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
## \[0.1.3]
- Split up fs and asset scopes. **This will reset the asset protocol scope once!**
- [ad30286](https://github.com/tauri-apps/plugins-workspace/commit/ad3028646c96ed213a2f483823ffdc3c17b5fc1e) fix(persisted-scope): separately save asset protocol patterns ([#459](https://github.com/tauri-apps/plugins-workspace/pull/459)) on 2023-07-10
## \[0.1.2]
- Fix usage of directory patterns by removing glob asterisks at the end before allowing/forbidding them. This was causing them to be escaped, and so undesirable paths were allowed/forbidden while polluting the `.persisted_scope` file.
- [9174b80](https://github.com/tauri-apps/plugins-workspace/commit/9174b808dc37154999c119fcc3f31258a9c5a3fb) \[persisted scope] fix: handle recursive directory correctly ([#455](https://github.com/tauri-apps/plugins-workspace/pull/455)) on 2023-06-29
## \[0.1.1] ## \[0.1.1]
- The MSRV was raised to 1.64! - The MSRV was raised to 1.64!

@ -1,4 +1,4 @@
![plugin-persisted-scope](banner.png) ![plugin-persisted-scope](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/persisted-scope/banner.png)
Save filesystem and asset scopes and restore them when the app is reopened. Save filesystem and asset scopes and restore them when the app is reopened.

@ -2,21 +2,36 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/persisted-scope/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/persisted-scope)
//!
//! Save filesystem and asset scopes and restore them when the app is reopened.
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
use aho_corasick::AhoCorasick; use aho_corasick::AhoCorasick;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::{ use tauri::{
plugin::{Builder, TauriPlugin}, plugin::{Builder, TauriPlugin},
AppHandle, Manager, Runtime, GlobPattern, Manager, Runtime,
}; };
use tauri_plugin_fs::{FsExt, ScopeEvent as FsScopeEvent}; #[cfg(feature = "protocol-asset")]
use tauri::{FsScope, FsScopeEvent};
use tauri_plugin_fs::{FsExt, Scope as FsPluginScope, ScopeEvent as FsPluginScopeEvent};
use std::{ use std::{
collections::HashSet,
fs::{create_dir_all, File}, fs::{create_dir_all, File},
io::Write, io::Write,
path::Path, path::Path,
}; };
// Using 2 separate files so that we don't have to think about write conflicts and not break backwards compat.
const SCOPE_STATE_FILENAME: &str = ".persisted-scope"; const SCOPE_STATE_FILENAME: &str = ".persisted-scope";
#[cfg(feature = "protocol-asset")]
const ASSET_SCOPE_STATE_FILENAME: &str = ".persisted-scope-asset";
// Most of these patterns are just added to try to fix broken files in the wild. // Most of these patterns are just added to try to fix broken files in the wild.
// After a while we can hopefully reduce it to something like [r"[?]", r"[*]", r"\\?\\\?\"] // After a while we can hopefully reduce it to something like [r"[?]", r"[*]", r"\\?\\\?\"]
@ -31,6 +46,70 @@ const PATTERNS: &[&str] = &[
]; ];
const REPLACE_WITH: &[&str] = &[r"[", r"]", r"?", r"*", r"\?", r"\\?\", r"\\?\"]; const REPLACE_WITH: &[&str] = &[r"[", r"]", r"?", r"*", r"\?", r"\\?\", r"\\?\"];
trait ScopeExt {
fn allow_file(&self, path: &Path);
fn allow_directory(&self, path: &Path, recursive: bool);
fn forbid_file(&self, path: &Path);
fn forbid_directory(&self, path: &Path, recursive: bool);
fn allowed_patterns(&self) -> HashSet<GlobPattern>;
fn forbidden_patterns(&self) -> HashSet<GlobPattern>;
}
impl ScopeExt for &FsPluginScope {
fn allow_file(&self, path: &Path) {
let _ = FsPluginScope::allow_file(self, path);
}
fn allow_directory(&self, path: &Path, recursive: bool) {
let _ = FsPluginScope::allow_directory(self, path, recursive);
}
fn forbid_file(&self, path: &Path) {
let _ = FsPluginScope::forbid_file(self, path);
}
fn forbid_directory(&self, path: &Path, recursive: bool) {
let _ = FsPluginScope::forbid_directory(self, path, recursive);
}
fn allowed_patterns(&self) -> HashSet<GlobPattern> {
FsPluginScope::allowed_patterns(self)
}
fn forbidden_patterns(&self) -> HashSet<GlobPattern> {
FsPluginScope::forbidden_patterns(self)
}
}
#[cfg(feature = "protocol-asset")]
impl ScopeExt for &FsScope {
fn allow_file(&self, path: &Path) {
let _ = FsScope::allow_file(self, path);
}
fn allow_directory(&self, path: &Path, recursive: bool) {
let _ = FsScope::allow_directory(self, path, recursive);
}
fn forbid_file(&self, path: &Path) {
let _ = FsScope::forbid_file(self, path);
}
fn forbid_directory(&self, path: &Path, recursive: bool) {
let _ = FsScope::forbid_directory(self, path, recursive);
}
fn allowed_patterns(&self) -> HashSet<GlobPattern> {
FsScope::allowed_patterns(self)
}
fn forbidden_patterns(&self) -> HashSet<GlobPattern> {
FsScope::forbidden_patterns(self)
}
}
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
enum Error { enum Error {
#[error(transparent)] #[error(transparent)]
@ -43,6 +122,14 @@ enum Error {
Bincode(#[from] Box<bincode::ErrorKind>), Bincode(#[from] Box<bincode::ErrorKind>),
} }
#[derive(Debug, Default, Deserialize, Serialize, Eq, PartialEq, Hash)]
enum TargetType {
#[default]
File,
Directory,
RecursiveDirectory,
}
#[derive(Debug, Default, Deserialize, Serialize)] #[derive(Debug, Default, Deserialize, Serialize)]
struct Scope { struct Scope {
allowed_paths: Vec<String>, allowed_paths: Vec<String>,
@ -59,85 +146,173 @@ fn fix_pattern(ac: &AhoCorasick, s: &str) -> String {
s s
} }
fn save_scopes<R: Runtime>(app: &AppHandle<R>, app_dir: &Path, scope_state_path: &Path) { const RESURSIVE_DIRECTORY_SUFFIX: &str = "**";
if let Some(fs_scope) = app.try_fs_scope() { const DIRECTORY_SUFFIX: &str = "*";
let scope = Scope {
allowed_paths: fs_scope fn detect_scope_type(scope_state_path: &str) -> TargetType {
.allowed_patterns() if scope_state_path.ends_with(RESURSIVE_DIRECTORY_SUFFIX) {
.into_iter() TargetType::RecursiveDirectory
.map(|p| p.to_string()) } else if scope_state_path.ends_with(DIRECTORY_SUFFIX) {
.collect(), TargetType::Directory
forbidden_patterns: fs_scope } else {
.forbidden_patterns() TargetType::File
.into_iter() }
.map(|p| p.to_string()) }
.collect(),
fn fix_directory(path_str: &str) -> &Path {
let mut path = Path::new(path_str);
if path.ends_with(DIRECTORY_SUFFIX) || path.ends_with(RESURSIVE_DIRECTORY_SUFFIX) {
path = match path.parent() {
Some(value) => value,
None => return path,
}; };
}
path
}
fn allow_path(scope: impl ScopeExt, path: &str) {
let target_type = detect_scope_type(path);
match target_type {
TargetType::File => {
scope.allow_file(Path::new(path));
}
TargetType::Directory => {
// We remove the '*' at the end of it, else it will be escaped by the pattern.
scope.allow_directory(fix_directory(path), false);
}
TargetType::RecursiveDirectory => {
// We remove the '**' at the end of it, else it will be escaped by the pattern.
scope.allow_directory(fix_directory(path), true);
}
}
}
let _ = create_dir_all(app_dir) fn forbid_path(scope: impl ScopeExt, path: &str) {
.and_then(|_| File::create(scope_state_path)) let target_type = detect_scope_type(path);
.map_err(Error::Io)
.and_then(|mut f| { match target_type {
f.write_all(&bincode::serialize(&scope).map_err(Error::from)?) TargetType::File => {
.map_err(Into::into) scope.forbid_file(Path::new(path));
}); }
TargetType::Directory => {
scope.forbid_directory(fix_directory(path), false);
}
TargetType::RecursiveDirectory => {
scope.forbid_directory(fix_directory(path), true);
}
} }
} }
fn save_scopes(scope: impl ScopeExt, app_dir: &Path, scope_state_path: &Path) {
let scope = Scope {
allowed_paths: scope
.allowed_patterns()
.into_iter()
.map(|p| p.to_string())
.collect(),
forbidden_patterns: scope
.forbidden_patterns()
.into_iter()
.map(|p| p.to_string())
.collect(),
};
let _ = create_dir_all(app_dir)
.and_then(|_| File::create(scope_state_path))
.map_err(Error::Io)
.and_then(|mut f| {
f.write_all(&bincode::serialize(&scope).map_err(Error::from)?)
.map_err(Into::into)
});
}
pub fn init<R: Runtime>() -> TauriPlugin<R> { pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("persisted-scope") Builder::new("persisted-scope")
.setup(|app, _api| { .setup(|app, _api| {
let fs_scope = app.try_fs_scope(); let fs_scope = app.try_fs_scope();
let core_scopes = app.state::<tauri::scope::Scopes>(); #[cfg(feature = "protocol-asset")]
let asset_protocol_scope = app.asset_protocol_scope();
let app = app.clone(); let app = app.clone();
let app_dir = app.path().app_data_dir(); let app_dir = app.path().app_data_dir();
if let Ok(app_dir) = app_dir { if let Ok(app_dir) = app_dir {
let scope_state_path = app_dir.join(SCOPE_STATE_FILENAME); let fs_scope_state_path = app_dir.join(SCOPE_STATE_FILENAME);
#[cfg(feature = "protocol-asset")]
let asset_scope_state_path = app_dir.join(ASSET_SCOPE_STATE_FILENAME);
if let Some(s) = fs_scope { if let Some(fs_scope) = fs_scope {
let _ = s.forbid_file(&scope_state_path); let _ = fs_scope.forbid_file(&fs_scope_state_path);}
} #[cfg(feature = "protocol-asset")]
let _ = core_scopes.forbid_file(&scope_state_path); let _ = asset_protocol_scope.forbid_file(&asset_scope_state_path);
// We're trying to fix broken .persisted-scope files seamlessly, so we'll be running this on the values read on the saved file. // We're trying to fix broken .persisted-scope files seamlessly, so we'll be running this on the values read on the saved file.
// We will still save some semi-broken values because the scope events are quite spammy and we don't want to reduce runtime performance any further. // We will still save some semi-broken values because the scope events are quite spammy and we don't want to reduce runtime performance any further.
let ac = AhoCorasick::new(PATTERNS).unwrap(/* This should be impossible to fail since we're using a small static input */); let ac = AhoCorasick::new(PATTERNS).unwrap(/* This should be impossible to fail since we're using a small static input */);
if scope_state_path.exists() { if let Some(fs_scope) = fs_scope {
let scope: Scope = tauri::api::file::read_binary(&scope_state_path) if fs_scope_state_path.exists() {
let scope: Scope = tauri::api::file::read_binary(&fs_scope_state_path)
.map_err(Error::from) .map_err(Error::from)
.and_then(|scope| bincode::deserialize(&scope).map_err(Into::into)) .and_then(|scope| bincode::deserialize(&scope).map_err(Into::into))
.unwrap_or_default(); .unwrap_or_default();
for allowed in &scope.allowed_paths { for allowed in &scope.allowed_paths {
let allowed = fix_pattern(&ac, allowed); let allowed = fix_pattern(&ac, allowed);
allow_path(fs_scope, &allowed);
if let Some(s) = fs_scope {
let _ = s.allow_file(&allowed);
}
let _ = core_scopes.allow_file(&allowed);
} }
for forbidden in &scope.forbidden_patterns { for forbidden in &scope.forbidden_patterns {
let forbidden = fix_pattern(&ac, forbidden); let forbidden = fix_pattern(&ac, forbidden);
forbid_path(fs_scope, &forbidden);
if let Some(s) = fs_scope {
let _ = s.forbid_file(&forbidden);
}
let _ = core_scopes.forbid_file(&forbidden);
} }
// Manually save the fixed scopes to disk once. // Manually save the fixed scopes to disk once.
// This is needed to fix broken .peristed-scope files in case the app doesn't update the scope itself. // This is needed to fix broken .peristed-scope files in case the app doesn't update the scope itself.
save_scopes(&app, &app_dir, &scope_state_path); save_scopes(fs_scope, &app_dir, &fs_scope_state_path);
}
}
#[cfg(feature = "protocol-asset")]
if asset_scope_state_path.exists() {
let scope: Scope = tauri::api::file::read_binary(&asset_scope_state_path)
.map_err(Error::from)
.and_then(|scope| bincode::deserialize(&scope).map_err(Into::into))
.unwrap_or_default();
for allowed in &scope.allowed_paths {
let allowed = fix_pattern(&ac, allowed);
allow_path(&asset_protocol_scope, &allowed);
}
for forbidden in &scope.forbidden_patterns {
let forbidden = fix_pattern(&ac, forbidden);
forbid_path(&asset_protocol_scope, &forbidden);
}
// Manually save the fixed scopes to disk once.
save_scopes(&asset_protocol_scope, &app_dir, &asset_scope_state_path);
} }
if let Some(s) = fs_scope { #[cfg(feature = "protocol-asset")]
s.listen(move |event| { let app_dir_ = app_dir.clone();
if let FsScopeEvent::PathAllowed(_) = event { if let Some(fs_scope) = fs_scope {
save_scopes(&app, &app_dir, &scope_state_path); let fs_scope_ = fs_scope.clone();
fs_scope.listen(move |event| {
if let FsPluginScopeEvent::PathAllowed(_) = event {
save_scopes(&fs_scope_, &app_dir, &fs_scope_state_path);
} }
}); });
} }
#[cfg(feature = "protocol-asset")]
{
let asset_protocol_scope_ = asset_protocol_scope.clone();
asset_protocol_scope.listen(move |event| {
if let FsScopeEvent::PathAllowed(_) = event {
save_scopes(&asset_protocol_scope_, &app_dir_, &asset_scope_state_path);
}
});}
} }
Ok(()) Ok(())
}) })

@ -1,4 +1,4 @@
![plugin-positioner](banner.png) ![plugin-positioner](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/positioner/banner.png)
Position your windows at well-known locations. Position your windows at well-known locations.

@ -25,7 +25,7 @@
"LICENSE" "LICENSE"
], ],
"devDependencies": { "devDependencies": {
"tslib": "^2.5.0" "tslib": "2.6.0"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.5" "@tauri-apps/api": "2.0.0-alpha.5"

@ -5,7 +5,7 @@ import { createConfig } from "../../shared/rollup.config.mjs";
export default createConfig({ export default createConfig({
input: "guest-js/index.ts", input: "guest-js/index.ts",
pkg: JSON.parse( pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8") readFileSync(new URL("./package.json", import.meta.url), "utf8"),
), ),
external: [/^@tauri-apps\/api/], external: [/^@tauri-apps\/api/],
}); });

@ -3,14 +3,20 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/positioner/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/positioner)
//!
//! A plugin for Tauri that helps position your windows at well-known locations. //! A plugin for Tauri that helps position your windows at well-known locations.
//! //!
//! # Cargo features //! # Cargo features
//! //!
//! - **system-tray**: Enables system-tray-relative positions. //! - **system-tray**: Enables system-tray-relative positions.
//! //!
//! Note: This requires attaching the Tauri plugin, *even* when using the trait extension only. //! Note: This requires attaching the Tauri plugin, *even* when using the trait extension only.
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
#![cfg(not(any(target_os = "android", target_os = "ios")))] #![cfg(not(any(target_os = "android", target_os = "ios")))]
mod ext; mod ext;

@ -1,4 +1,4 @@
![plugin-process](banner.png) ![plugin-process](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/process/banner.png)
This plugin provides APIs to access the current process. To spawn child processes, see the [`shell`](https://github.com/tauri-apps/tauri-plugin-shell) plugin. This plugin provides APIs to access the current process. To spawn child processes, see the [`shell`](https://github.com/tauri-apps/tauri-plugin-shell) plugin.

@ -24,7 +24,7 @@
"LICENSE" "LICENSE"
], ],
"devDependencies": { "devDependencies": {
"tslib": "^2.5.0" "tslib": "2.6.0"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "2.0.0-alpha.5" "@tauri-apps/api": "2.0.0-alpha.5"

@ -5,7 +5,7 @@ import { createConfig } from "../../shared/rollup.config.mjs";
export default createConfig({ export default createConfig({
input: "guest-js/index.ts", input: "guest-js/index.ts",
pkg: JSON.parse( pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8") readFileSync(new URL("./package.json", import.meta.url), "utf8"),
), ),
external: [/^@tauri-apps\/api/], external: [/^@tauri-apps\/api/],
}); });

@ -2,6 +2,15 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/process/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/process)
//!
//! This plugin provides APIs to access the current process. To spawn child processes, see the [`shell`](https://github.com/tauri-apps/tauri-plugin-shell) plugin.
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
use tauri::{ use tauri::{
plugin::{Builder, TauriPlugin}, plugin::{Builder, TauriPlugin},
Runtime, Runtime,

@ -1,4 +1,4 @@
![plugin-shell](banner.png) ![plugin-shell](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/shell/banner.png)
Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application. Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application.

@ -122,7 +122,7 @@ async function execute<O extends IOPayload>(
onEvent: (event: CommandEvent<O>) => void, onEvent: (event: CommandEvent<O>) => void,
program: string, program: string,
args: string | string[] = [], args: string | string[] = [],
options?: InternalSpawnOptions options?: InternalSpawnOptions,
): Promise<number> { ): Promise<number> {
if (typeof args === "object") { if (typeof args === "object") {
Object.freeze(args); Object.freeze(args);
@ -153,7 +153,7 @@ class EventEmitter<E extends Record<string, any>> {
*/ */
addListener<N extends keyof E>( addListener<N extends keyof E>(
eventName: N, eventName: N,
listener: (arg: E[typeof eventName]) => void listener: (arg: E[typeof eventName]) => void,
): this { ): this {
return this.on(eventName, listener); return this.on(eventName, listener);
} }
@ -165,7 +165,7 @@ class EventEmitter<E extends Record<string, any>> {
*/ */
removeListener<N extends keyof E>( removeListener<N extends keyof E>(
eventName: N, eventName: N,
listener: (arg: E[typeof eventName]) => void listener: (arg: E[typeof eventName]) => void,
): this { ): this {
return this.off(eventName, listener); return this.off(eventName, listener);
} }
@ -182,7 +182,7 @@ class EventEmitter<E extends Record<string, any>> {
*/ */
on<N extends keyof E>( on<N extends keyof E>(
eventName: N, eventName: N,
listener: (arg: E[typeof eventName]) => void listener: (arg: E[typeof eventName]) => void,
): this { ): this {
if (eventName in this.eventListeners) { if (eventName in this.eventListeners) {
// eslint-disable-next-line security/detect-object-injection // eslint-disable-next-line security/detect-object-injection
@ -204,7 +204,7 @@ class EventEmitter<E extends Record<string, any>> {
*/ */
once<N extends keyof E>( once<N extends keyof E>(
eventName: N, eventName: N,
listener: (arg: E[typeof eventName]) => void listener: (arg: E[typeof eventName]) => void,
): this { ): this {
const wrapper = (arg: E[typeof eventName]): void => { const wrapper = (arg: E[typeof eventName]): void => {
this.removeListener(eventName, wrapper); this.removeListener(eventName, wrapper);
@ -222,12 +222,12 @@ class EventEmitter<E extends Record<string, any>> {
*/ */
off<N extends keyof E>( off<N extends keyof E>(
eventName: N, eventName: N,
listener: (arg: E[typeof eventName]) => void listener: (arg: E[typeof eventName]) => void,
): this { ): this {
if (eventName in this.eventListeners) { if (eventName in this.eventListeners) {
// eslint-disable-next-line security/detect-object-injection // eslint-disable-next-line security/detect-object-injection
this.eventListeners[eventName] = this.eventListeners[eventName].filter( this.eventListeners[eventName] = this.eventListeners[eventName].filter(
(l) => l !== listener (l) => l !== listener,
); );
} }
return this; return this;
@ -295,7 +295,7 @@ class EventEmitter<E extends Record<string, any>> {
*/ */
prependListener<N extends keyof E>( prependListener<N extends keyof E>(
eventName: N, eventName: N,
listener: (arg: E[typeof eventName]) => void listener: (arg: E[typeof eventName]) => void,
): this { ): this {
if (eventName in this.eventListeners) { if (eventName in this.eventListeners) {
// eslint-disable-next-line security/detect-object-injection // eslint-disable-next-line security/detect-object-injection
@ -317,7 +317,7 @@ class EventEmitter<E extends Record<string, any>> {
*/ */
prependOnceListener<N extends keyof E>( prependOnceListener<N extends keyof E>(
eventName: N, eventName: N,
listener: (arg: E[typeof eventName]) => void listener: (arg: E[typeof eventName]) => void,
): this { ): this {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const wrapper = (arg: any): void => { const wrapper = (arg: any): void => {
@ -434,7 +434,7 @@ class Command<O extends IOPayload> extends EventEmitter<CommandEvents> {
private constructor( private constructor(
program: string, program: string,
args: string | string[] = [], args: string | string[] = [],
options?: SpawnOptions options?: SpawnOptions,
) { ) {
super(); super();
this.program = program; this.program = program;
@ -446,12 +446,12 @@ class Command<O extends IOPayload> extends EventEmitter<CommandEvents> {
static create( static create(
program: string, program: string,
args?: string | string[], args?: string | string[],
options?: SpawnOptions & { encoding: "raw" } options?: SpawnOptions & { encoding: "raw" },
): Command<Uint8Array>; ): Command<Uint8Array>;
static create( static create(
program: string, program: string,
args?: string | string[], args?: string | string[],
options?: SpawnOptions options?: SpawnOptions,
): Command<string>; ): Command<string>;
/** /**
@ -469,7 +469,7 @@ class Command<O extends IOPayload> extends EventEmitter<CommandEvents> {
static create<O extends IOPayload>( static create<O extends IOPayload>(
program: string, program: string,
args: string | string[] = [], args: string | string[] = [],
options?: SpawnOptions options?: SpawnOptions,
): Command<O> { ): Command<O> {
return new Command(program, args, options); return new Command(program, args, options);
} }
@ -478,12 +478,12 @@ class Command<O extends IOPayload> extends EventEmitter<CommandEvents> {
static sidecar( static sidecar(
program: string, program: string,
args?: string | string[], args?: string | string[],
options?: SpawnOptions & { encoding: "raw" } options?: SpawnOptions & { encoding: "raw" },
): Command<Uint8Array>; ): Command<Uint8Array>;
static sidecar( static sidecar(
program: string, program: string,
args?: string | string[], args?: string | string[],
options?: SpawnOptions options?: SpawnOptions,
): Command<string>; ): Command<string>;
/** /**
@ -501,7 +501,7 @@ class Command<O extends IOPayload> extends EventEmitter<CommandEvents> {
static sidecar<O extends IOPayload>( static sidecar<O extends IOPayload>(
program: string, program: string,
args: string | string[] = [], args: string | string[] = [],
options?: SpawnOptions options?: SpawnOptions,
): Command<O> { ): Command<O> {
const instance = new Command<O>(program, args, options); const instance = new Command<O>(program, args, options);
instance.options.sidecar = true; instance.options.sidecar = true;
@ -535,7 +535,7 @@ class Command<O extends IOPayload> extends EventEmitter<CommandEvents> {
}, },
this.program, this.program,
this.args, this.args,
this.options this.options,
).then((pid) => new Child(pid)); ).then((pid) => new Child(pid));
} }

@ -5,7 +5,7 @@ import { createConfig } from "../../shared/rollup.config.mjs";
export default createConfig({ export default createConfig({
input: "guest-js/index.ts", input: "guest-js/index.ts",
pkg: JSON.parse( pkg: JSON.parse(
readFileSync(new URL("./package.json", import.meta.url), "utf8") readFileSync(new URL("./package.json", import.meta.url), "utf8"),
), ),
external: [/^@tauri-apps\/api/], external: [/^@tauri-apps\/api/],
}); });

@ -12,7 +12,7 @@
if ( if (
target.href && target.href &&
["http://", "https://", "mailto:", "tel:"].some((v) => ["http://", "https://", "mailto:", "tel:"].some((v) =>
target.href.startsWith(v) target.href.startsWith(v),
) && ) &&
target.target === "_blank" target.target === "_blank"
) { ) {

@ -2,8 +2,19 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/shell/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/shell)
//!
//! Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application.
#![doc(
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
)]
use std::{ use std::{
collections::HashMap, collections::HashMap,
ffi::OsStr,
path::Path,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
@ -36,7 +47,7 @@ pub struct Shell<R: Runtime> {
impl<R: Runtime> Shell<R> { impl<R: Runtime> Shell<R> {
/// Creates a new Command for launching the given program. /// Creates a new Command for launching the given program.
pub fn command(&self, program: impl Into<String>) -> Command { pub fn command(&self, program: impl AsRef<OsStr>) -> Command {
Command::new(program) Command::new(program)
} }
@ -44,7 +55,7 @@ impl<R: Runtime> Shell<R> {
/// ///
/// A sidecar program is a embedded external binary in order to make your application work /// A sidecar program is a embedded external binary in order to make your application work
/// or to prevent users having to install additional dependencies (e.g. Node.js, Python, etc). /// or to prevent users having to install additional dependencies (e.g. Node.js, Python, etc).
pub fn sidecar(&self, program: impl Into<String>) -> Result<Command> { pub fn sidecar(&self, program: impl AsRef<Path>) -> Result<Command> {
Command::new_sidecar(program) Command::new_sidecar(program)
} }

@ -3,9 +3,9 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use std::{ use std::{
collections::HashMap, ffi::OsStr,
io::{BufReader, Write}, io::{BufReader, Write},
path::PathBuf, path::{Path, PathBuf},
process::{Command as StdCommand, Stdio}, process::{Command as StdCommand, Stdio},
sync::{Arc, RwLock}, sync::{Arc, RwLock},
thread::spawn, thread::spawn,
@ -53,13 +53,7 @@ pub enum CommandEvent {
/// The type to spawn commands. /// The type to spawn commands.
#[derive(Debug)] #[derive(Debug)]
pub struct Command { pub struct Command(StdCommand);
program: String,
args: Vec<String>,
env_clear: bool,
env: HashMap<String, String>,
current_dir: Option<PathBuf>,
}
/// Spawned child process. /// Spawned child process.
#[derive(Debug)] #[derive(Debug)]
@ -116,49 +110,44 @@ pub struct Output {
pub stderr: Vec<u8>, pub stderr: Vec<u8>,
} }
fn relative_command_path(command: String) -> crate::Result<String> { fn relative_command_path(command: &Path) -> crate::Result<PathBuf> {
match platform::current_exe()?.parent() { match platform::current_exe()?.parent() {
#[cfg(windows)] #[cfg(windows)]
Some(exe_dir) => Ok(format!("{}\\{command}.exe", exe_dir.display())), Some(exe_dir) => Ok(exe_dir.join(command).with_extension("exe")),
#[cfg(not(windows))] #[cfg(not(windows))]
Some(exe_dir) => Ok(format!("{}/{command}", exe_dir.display())), Some(exe_dir) => Ok(exe_dir.join(command)),
None => Err(crate::Error::CurrentExeHasNoParent), None => Err(crate::Error::CurrentExeHasNoParent),
} }
} }
impl From<Command> for StdCommand { impl From<Command> for StdCommand {
fn from(cmd: Command) -> StdCommand { fn from(cmd: Command) -> StdCommand {
let mut command = StdCommand::new(cmd.program); cmd.0
command.args(cmd.args); }
}
impl Command {
pub(crate) fn new<S: AsRef<OsStr>>(program: S) -> Self {
let mut command = StdCommand::new(program);
command.stdout(Stdio::piped()); command.stdout(Stdio::piped());
command.stdin(Stdio::piped()); command.stdin(Stdio::piped());
command.stderr(Stdio::piped()); command.stderr(Stdio::piped());
if cmd.env_clear {
command.env_clear();
}
command.envs(cmd.env);
if let Some(current_dir) = cmd.current_dir {
command.current_dir(current_dir);
}
#[cfg(windows)] #[cfg(windows)]
command.creation_flags(CREATE_NO_WINDOW); command.creation_flags(CREATE_NO_WINDOW);
command
Self(command)
} }
}
impl Command { pub(crate) fn new_sidecar<S: AsRef<Path>>(program: S) -> crate::Result<Self> {
pub(crate) fn new<S: Into<String>>(program: S) -> Self { Ok(Self::new(relative_command_path(program.as_ref())?))
Self {
program: program.into(),
args: Default::default(),
env_clear: false,
env: Default::default(),
current_dir: None,
}
} }
pub(crate) fn new_sidecar<S: Into<String>>(program: S) -> crate::Result<Self> { /// Appends an argument to the command.
Ok(Self::new(relative_command_path(program.into())?)) #[must_use]
pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
self.0.arg(arg);
self
} }
/// Appends arguments to the command. /// Appends arguments to the command.
@ -166,32 +155,46 @@ impl Command {
pub fn args<I, S>(mut self, args: I) -> Self pub fn args<I, S>(mut self, args: I) -> Self
where where
I: IntoIterator<Item = S>, I: IntoIterator<Item = S>,
S: AsRef<str>, S: AsRef<OsStr>,
{ {
for arg in args { self.0.args(args);
self.args.push(arg.as_ref().to_string());
}
self self
} }
/// Clears the entire environment map for the child process. /// Clears the entire environment map for the child process.
#[must_use] #[must_use]
pub fn env_clear(mut self) -> Self { pub fn env_clear(mut self) -> Self {
self.env_clear = true; self.0.env_clear();
self
}
/// Inserts or updates an explicit environment variable mapping.
#[must_use]
pub fn env<K, V>(mut self, key: K, value: V) -> Self
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.0.env(key, value);
self self
} }
/// Adds or updates multiple environment variable mappings. /// Adds or updates multiple environment variable mappings.
#[must_use] #[must_use]
pub fn envs(mut self, env: HashMap<String, String>) -> Self { pub fn envs<I, K, V>(mut self, envs: I) -> Self
self.env = env; where
I: IntoIterator<Item = (K, V)>,
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.0.envs(envs);
self self
} }
/// Sets the working directory for the child process. /// Sets the working directory for the child process.
#[must_use] #[must_use]
pub fn current_dir(mut self, current_dir: PathBuf) -> Self { pub fn current_dir<P: AsRef<Path>>(mut self, current_dir: P) -> Self {
self.current_dir.replace(current_dir); self.0.current_dir(current_dir);
self self
} }

@ -264,8 +264,8 @@ impl Scope {
// The prevention of argument escaping is handled by the usage of std::process::Command::arg by // The prevention of argument escaping is handled by the usage of std::process::Command::arg by
// the `open` dependency. This behavior should be re-confirmed during upgrades of `open`. // the `open` dependency. This behavior should be re-confirmed during upgrades of `open`.
match with.map(Program::name) { match with.map(Program::name) {
Some(program) => ::open::with(path, program), Some(program) => ::open::with_detached(path, program),
None => ::open::that(path), None => ::open::that_detached(path),
} }
.map_err(Into::into) .map_err(Into::into)
} }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save