Merge branch 'v2' into feat/http/persist-cookies

pull/1978/head
Amr Bashir 7 months ago committed by GitHub
commit e4eed02b3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -56,12 +56,13 @@
"dependencies": [
"barcode-scanner",
"biometric",
"log-plugin",
"log",
"cli",
"clipboard-manager",
"dialog",
"fs",
"global-shortcut",
"opener",
"http",
"nfc",
"notification",
@ -87,6 +88,7 @@
"dialog-js",
"fs-js",
"global-shortcut-js",
"opener-js",
"http-js",
"nfc-js",
"notification-js",
@ -168,7 +170,8 @@
},
"dialog-js": {
"path": "./plugins/dialog",
"manager": "javascript"
"manager": "javascript",
"dependencies": ["fs-js"]
},
"geolocation": {
"path": "./plugins/geolocation",
@ -186,6 +189,14 @@
"path": "./plugins/global-shortcut",
"manager": "javascript"
},
"opener": {
"path": "./plugins/opener",
"manager": "rust"
},
"opener-js": {
"path": "./plugins/opener",
"manager": "javascript"
},
"haptics": {
"path": "./plugins/haptics",
"manager": "rust"
@ -201,13 +212,14 @@
},
"http-js": {
"path": "./plugins/http",
"manager": "javascript"
"manager": "javascript",
"dependencies": ["fs-js"]
},
"localhost": {
"path": "./plugins/localhost",
"manager": "rust"
},
"log-plugin": {
"log": {
"path": "./plugins/log",
"manager": "rust"
},

@ -1,5 +0,0 @@
---
sql: patch
---
Replace `Mutex` with `RwLock` to enable concurrent SQL execution.

@ -1,5 +0,0 @@
---
"fs-js": "patch"
---
Fix `writeTextFile` converting UTF-8 characters (for example `äöü`) in the given path into replacement character (`<60>`)

@ -0,0 +1,6 @@
---
"log": patch
"log-js": patch
---
Make log functions omit caller location when failed to parse it instead of throwing

@ -6,6 +6,8 @@ As you create PRs and make changes that require a version bump, please add a new
When you select the version bump required, you do _not_ need to consider dependencies. Only note the package with the actual change, and any packages that depend on that package will be bumped automatically in the process.
**Note, that in this repository, even if only the Rust code or only the JavaScript code of a plugin changed, both packages need to be bumped with the same increment!**
Use the following format:
```md

@ -0,0 +1,44 @@
# Copyright 2019-2023 Tauri Programme within The Commons Conservancy
# SPDX-License-Identifier: Apache-2.0
# SPDX-License-Identifier: MIT
name: check change files
on:
pull_request:
paths:
- '.changes/*.md'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: check change files end with .md
run: |
for file in .changes/*
do
if [[ ! "$file" =~ \.(md|json)$ ]]; then
echo ".changes directory should only contain files that end with .md"
echo "found an invalid file in .changes directory:"
echo "$file"
exit 1
fi
done
- uses: dorny/paths-filter@v3
id: filter
with:
list-files: shell
filters: |
changes:
- added|modified: '.changes/*.md'
- name: check
run: node ./.scripts/ci/check-change-files.js ${{ steps.filter.outputs.changes_files }}
if: ${{ steps.filter.outputs.changes == 'true' }}

@ -53,6 +53,10 @@ jobs:
- .github/workflows/check-generated-files.yml
- plugins/global-shortcut/guest-js/**
- plugins/global-shortcut/src/api-iife.js
opener:
- .github/workflows/check-generated-files.yml
- plugins/opener/guest-js/**
- plugins/opener/src/api-iife.js
haptics:
- .github/workflows/check-generated-files.yml
- plugins/haptics/guest-js/**

@ -66,6 +66,9 @@ jobs:
tauri-plugin-global-shortcut:
- .github/workflows/lint-rust.yml
- plugins/global-shortcut/**
tauri-plugin-opener:
- .github/workflows/lint-rust.yml
- plugins/opener/**
tauri-plugin-haptics:
- .github/workflows/lint-rust.yml
- plugins/haptics/**

@ -47,109 +47,140 @@ jobs:
tauri-plugin-autostart:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/autostart/**
tauri-plugin-cli:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/cli/**
tauri-plugin-clipboard-manager:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/clipboard-manager/**
tauri-plugin-deep-link:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/deep-link/**
tauri-plugin-dialog:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/dialog/**
- plugins/fs/**
tauri-plugin-fs:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/fs/**
tauri-plugin-geolocation:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/geolocation/**
tauri-plugin-global-shortcut:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/global-shortcut/**
tauri-plugin-opener:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/opener/**
tauri-plugin-haptics:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/haptics/**
tauri-plugin-http:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/http/**
- plugins/fs/**
tauri-plugin-localhost:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/localhost/**
tauri-plugin-log:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/log/**
tauri-plugin-notification:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/notification/**
tauri-plugin-os:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/os/**
tauri-plugin-persisted-scope:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/persisted-scope/**
- plugins/fs/**
tauri-plugin-positioner:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/positioner/**
tauri-plugin-process:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/process/**
tauri-plugin-shell:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/shell/**
tauri-plugin-single-instance:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/single-instance/**
tauri-plugin-sql:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/sql/**
tauri-plugin-store:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/store/**
tauri-plugin-stronghold:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/stronghold/**
tauri-plugin-updater:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/updater/**
tauri-plugin-upload:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/upload/**
tauri-plugin-websocket:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/websocket/**
tauri-plugin-window-state:
- .github/workflows/test-rust.yml
- Cargo.toml
- Cargo.lock
- plugins/window-state/**
test:

@ -0,0 +1,86 @@
#!/usr/bin/env node
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
import { readFileSync, readdirSync } from 'fs'
import { join } from 'path'
/* const ignorePackages = [
'api-example',
'api-example-js',
'deep-link-example',
'deep-link-example-js'
] */
const rsOnly = ['localhost', 'persisted-scope']
function checkChangeFiles(changeFiles) {
let code = 0
for (const file of changeFiles) {
const content = readFileSync(file, 'utf8')
const [frontMatter] = /^---[\s\S.]*---\n/i.exec(content)
const packages = frontMatter
.split('\n')
.filter((l) => !(l === '---' || !l))
.map((l) => l.replace(/('|")/g, '').split(':'))
const rsPackages = Object.fromEntries(
packages
.filter((v) => !v[0].endsWith('-js'))
.map((v) => [v[0], v[1].trim()])
)
const jsPackages = Object.fromEntries(
packages
.filter((v) => v[0].endsWith('-js'))
.map((v) => [v[0].slice(0, -3), v[1].trim()])
)
for (const pkg in rsPackages) {
if (rsOnly.includes(pkg)) continue
if (!jsPackages[pkg]) {
console.error(
`Missing "${rsPackages[pkg]}" bump for JS package "${pkg}-js" in ${file}.`
)
code = 1
} else if (rsPackages[pkg] != jsPackages[pkg]) {
console.error(
`"${pkg}" and "${pkg}-js" have different version bumps in ${file}.`
)
code = 1
}
}
for (const pkg in jsPackages) {
if (!rsPackages[pkg]) {
console.error(
`Missing "${jsPackages[pkg]}" bump for Rust package "${pkg}" in ${file}.`
)
code = 1
} else if (rsPackages[pkg] != jsPackages[pkg]) {
console.error(
`"${pkg}" and "${pkg}-js" have different version bumps in ${file}.`
)
code = 1
}
}
}
process.exit(code)
}
const [_bin, _script, ...files] = process.argv
if (files.length > 0) {
checkChangeFiles(
files.filter((f) => f.toLowerCase() !== '.changes/readme.md')
)
} else {
const changeFiles = readdirSync('.changes')
.filter((f) => f.endsWith('.md') && f.toLowerCase() !== 'readme.md')
.map((p) => join('.changes', p))
checkChangeFiles(changeFiles)
}

@ -0,0 +1 @@
plugins/*/permissions/autogenerated/

1947
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -10,17 +10,20 @@ resolver = "2"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tracing = "0.1"
log = "0.4"
tauri = { version = "2.0.4", default-features = false }
tauri-build = "2.0.1"
tauri-plugin = "2.0.1"
tauri-utils = "2.0.1"
tauri = { version = "2", default-features = false }
tauri-build = "2"
tauri-plugin = "2"
tauri-utils = "2"
serde_json = "1"
thiserror = "1"
thiserror = "2"
url = "2"
schemars = "0.8"
dunce = "1"
specta = "=2.0.0-rc.20"
glob = "0.3"
zbus = "4"
#tauri-specta = "=2.0.0-rc.11"
[workspace.package]

@ -1,36 +1,45 @@
# Official Tauri Plugins
This repo and all plugins require a Rust version of at least **1.77.2**
## Plugins Found Here
| | | Win | Mac | Lin | iOS | And |
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | --- | --- | --- | --- |
| [autostart](plugins/autostart) | Automatically launch your app at system startup. | ✅ | ✅ | ✅ | ? | ? |
| [autostart](plugins/autostart) | Automatically launch your app at system startup. | ✅ | ✅ | ✅ | ❌ | ❌ |
| [barcode-scanner](plugins/barcode-scanner) | Allows your mobile application to use the camera to scan QR codes, EAN-13 and other kinds of barcodes. | ? | ? | ? | ✅ | ✅ |
| [biometric](plugins/biometric) | Prompt the user for biometric authentication on Android and iOS. | ? | ? | ? | ✅ | ✅ |
| [cli](plugins/cli) | Parse arguments from your Command Line Interface | ✅ | ✅ | ✅ | ? | ? |
| [cli](plugins/cli) | Parse arguments from your Command Line Interface | ✅ | ✅ | ✅ | ❌ | ❌ |
| [clipboard-manager](plugins/clipboard-manager) | Read and write to the system clipboard. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [deep-link](plugins/deep-link) | Set your Tauri application as the default handler for an URL. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [dialog](plugins/dialog) | Native system dialogs for opening and saving files along with message dialogs. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [fs](plugins/fs) | Access the file system. | ✅ | ✅ | ✅ | ? | ? |
| [geolocation](plugins/geolocation) | Get and track current device position. | ? | ? | ? | ✅ | ✅ |
| [global-shortcut](plugins/global-shortcut) | Register global shortcuts. | ✅ | ✅ | ✅ | ? | ? |
| [haptics](plugins/haptics) | Haptic feedback and vibrations. | ? | ? | ? | ✅ | ✅ |
| [http](plugins/http) | Access the HTTP client written in Rust. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [localhost](plugins/localhost) | Use a localhost server in production apps. | ✅ | ✅ | ✅ | ? | ? |
| [log](plugins/log) | Configurable logging. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [nfc](plugins/nfc) | Read and write NFC tags on Android and iOS. | ? | ? | ? | ✅ | ✅ |
| [notification](plugins/notification) | Send message notifications (brief auto-expiring OS window element) to your user. Can also be used with the Notification Web API. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [opener](plugins/opener) | Open files and URLs using their default application. | ✅ | ✅ | ✅ | ? | ? |
| [os](plugins/os) | Read information about the operating system. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [persisted-scope](plugins/persisted-scope) | Persist runtime scope changes on the filesystem. | ✅ | ✅ | ✅ | ? | ? |
| [positioner](plugins/positioner) | Move windows to common locations. | ✅ | ✅ | ✅ | ? | ? |
| [positioner](plugins/positioner) | Move windows to common locations. | ✅ | ✅ | ✅ | ❌ | ❌ |
| [process](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. | ✅ | ✅ | ✅ | ? | ? |
| [shell](plugins/shell) | Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application. | ✅ | ✅ | ✅ | ? | ? |
| [single-instance](plugins/single-instance) | Ensure a single instance of your tauri app is running. | ✅ | ? | ✅ | ? | ? |
| [sql](plugins/sql) | Interface with SQL databases. | ✅ | ✅ | ✅ | ? | ? |
| [single-instance](plugins/single-instance) | Ensure a single instance of your tauri app is running. | ✅ | ✅ | ✅ | ❌ | ❌ |
| [sql](plugins/sql) | Interface with SQL databases. | ✅ | ✅ | ✅ | ? | |
| [store](plugins/store) | Persistent key value storage. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [stronghold](plugins/stronghold) | Encrypted, secure database. | ✅ | ✅ | ✅ | ? | ? |
| [updater](plugins/updater) | In-app updates for Tauri applications. | ✅ | ✅ | ✅ | ? | ? |
| [updater](plugins/updater) | In-app updates for Tauri applications. | ✅ | ✅ | ✅ | ❌ | ❌ |
| [upload](plugins/upload) | Tauri plugin for file uploads through HTTP. | ✅ | ✅ | ✅ | ? | ? |
| [websocket](plugins/websocket) | Open a WebSocket connection using a Rust client in JS. | ✅ | ✅ | ✅ | ? | ? |
| [window-state](plugins/window-state) | Persist window sizes and positions. | ✅ | ✅ | ✅ | ? | ? |
| [window-state](plugins/window-state) | Persist window sizes and positions. | ✅ | ✅ | ✅ | ❌ | ❌ |
_This repo and all plugins require a Rust version of at least **1.77.2**_
- ✅: (Partially) Supported
- ❌: Not supported
- `?` : Unknown/Untested or Planned
## Contributing

@ -1,5 +1,20 @@
# Changelog
## \[2.0.3]
### Dependencies
- Upgraded to `clipboard-manager-js@2.0.1`
- Upgraded to `log-js@2.0.1`
- Upgraded to `fs-js@2.0.3`
- Upgraded to `opener-js@2.0.0`
## \[2.0.2]
### Dependencies
- Upgraded to `fs-js@2.0.2`
## \[2.0.1]
### Dependencies

@ -1,23 +1,25 @@
{
"name": "svelte-app",
"name": "api",
"private": true,
"version": "2.0.1",
"version": "2.0.3",
"type": "module",
"scripts": {
"dev": "vite --clearScreen false",
"build": "vite build",
"serve": "vite preview"
"serve": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "2.0.2",
"@tauri-apps/api": "2.1.1",
"@tauri-apps/plugin-barcode-scanner": "2.0.0",
"@tauri-apps/plugin-biometric": "2.0.0",
"@tauri-apps/plugin-cli": "2.0.0",
"@tauri-apps/plugin-clipboard-manager": "2.0.0",
"@tauri-apps/plugin-clipboard-manager": "2.0.1",
"@tauri-apps/plugin-dialog": "2.0.1",
"@tauri-apps/plugin-fs": "2.0.1",
"@tauri-apps/plugin-fs": "2.0.3",
"@tauri-apps/plugin-geolocation": "2.0.0",
"@tauri-apps/plugin-global-shortcut": "2.0.0",
"@tauri-apps/plugin-opener": "2.0.0",
"@tauri-apps/plugin-haptics": "2.0.0",
"@tauri-apps/plugin-http": "2.0.1",
"@tauri-apps/plugin-nfc": "2.0.0",
@ -32,11 +34,11 @@
"devDependencies": {
"@iconify-json/codicon": "^1.1.37",
"@iconify-json/ph": "^1.1.8",
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@tauri-apps/cli": "2.0.3",
"@unocss/extractor-svelte": "^0.63.0",
"svelte": "^4.2.19",
"unocss": "^0.63.0",
"vite": "^5.4.7"
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tauri-apps/cli": "2.1.0",
"@unocss/extractor-svelte": "^0.65.0",
"svelte": "^5.0.0",
"unocss": "^0.65.0",
"vite": "^6.0.0"
}
}

@ -1,5 +1,23 @@
# Changelog
## \[2.0.6]
### Dependencies
- Upgraded to `fs@2.1.0`
- Upgraded to `updater@2.1.0`
- Upgraded to `dialog@2.0.4`
- Upgraded to `log-plugin@2.0.3`
- Upgraded to `http@2.0.4`
- Upgraded to `opener@2.0.0`
## \[2.0.5]
### Dependencies
- Upgraded to `clipboard-manager@2.0.2`
- Upgraded to `log-plugin@2.0.2`
## \[2.0.4]
### Dependencies

@ -1,7 +1,7 @@
[package]
name = "api"
publish = false
version = "2.0.4"
version = "2.0.6"
description = "An example Tauri Application showcasing the api"
edition = "2021"
rust-version = { workspace = true }
@ -19,20 +19,21 @@ serde_json = { workspace = true }
serde = { workspace = true }
tiny_http = "0.12"
log = { workspace = true }
tauri-plugin-log = { path = "../../../plugins/log", version = "2.0.1" }
tauri-plugin-fs = { path = "../../../plugins/fs", version = "2.0.3", features = [
tauri-plugin-log = { path = "../../../plugins/log", version = "2.0.3" }
tauri-plugin-fs = { path = "../../../plugins/fs", version = "2.1.0", features = [
"watch",
] }
tauri-plugin-clipboard-manager = { path = "../../../plugins/clipboard-manager", version = "2.0.1" }
tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.0.3" }
tauri-plugin-clipboard-manager = { path = "../../../plugins/clipboard-manager", version = "2.0.2" }
tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.0.4" }
tauri-plugin-http = { path = "../../../plugins/http", features = [
"multipart",
], version = "2.0.3" }
], version = "2.0.4" }
tauri-plugin-notification = { path = "../../../plugins/notification", version = "2.0.1", features = [
"windows7-compat",
] }
tauri-plugin-os = { path = "../../../plugins/os", version = "2.0.1" }
tauri-plugin-process = { path = "../../../plugins/process", version = "2.0.1" }
tauri-plugin-opener = { path = "../../../plugins/opener", version = "2.0.0" }
tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.0.2" }
tauri-plugin-store = { path = "../../../plugins/store", version = "2.1.0" }
@ -52,7 +53,7 @@ features = [
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
tauri-plugin-cli = { path = "../../../plugins/cli", version = "2.0.1" }
tauri-plugin-global-shortcut = { path = "../../../plugins/global-shortcut", version = "2.0.1" }
tauri-plugin-updater = { path = "../../../plugins/updater", version = "2.0.2" }
tauri-plugin-updater = { path = "../../../plugins/updater", version = "2.1.0" }
tauri-plugin-window-state = { path = "../../../plugins/window-state", version = "2.0.0" }
[target."cfg(any(target_os = \"android\", target_os = \"ios\"))".dependencies]

@ -53,6 +53,7 @@
}
]
},
"shell:allow-open",
"shell:allow-kill",
"shell:allow-stdin-write",
"process:allow-exit",
@ -79,6 +80,11 @@
],
"deny": ["$APPDATA/db/*.stronghold"]
},
"store:default"
"store:default",
"opener:default",
{
"identifier": "opener:allow-open-path",
"allow": [{ "path": "$APPDATA" }, { "path": "$APPDATA/**" }]
}
]
}

@ -9,6 +9,8 @@
"updater:default",
"global-shortcut:allow-unregister",
"global-shortcut:allow-register",
"global-shortcut:allow-unregister-all"
"global-shortcut:allow-unregister-all",
{ "identifier": "fs:allow-watch", "allow": ["*", "**/*"] },
"fs:allow-unwatch"
]
}

@ -15,6 +15,10 @@
"geolocation:allow-check-permissions",
"geolocation:allow-request-permissions",
"geolocation:allow-watch-position",
"geolocation:allow-get-current-position"
"geolocation:allow-get-current-position",
"haptics:allow-impact-feedback",
"haptics:allow-notification-feedback",
"haptics:allow-selection-feedback",
"haptics:allow-vibrate"
]
}

@ -36,6 +36,7 @@ pub fn run() {
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_store::Builder::default().build())
.setup(move |app| {

@ -1,6 +1,5 @@
<script>
import { writable } from 'svelte/store'
import { open } from '@tauri-apps/plugin-shell'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { getCurrentWebview } from '@tauri-apps/api/webview'
import * as os from '@tauri-apps/plugin-os'
@ -14,6 +13,7 @@
import Notifications from './views/Notifications.svelte'
import Shortcuts from './views/Shortcuts.svelte'
import Shell from './views/Shell.svelte'
import Opener from './views/Opener.svelte'
import Store from './views/Store.svelte'
import Updater from './views/Updater.svelte'
import Clipboard from './views/Clipboard.svelte'
@ -21,6 +21,7 @@
import Scanner from './views/Scanner.svelte'
import Biometric from './views/Biometric.svelte'
import Geolocation from './views/Geolocation.svelte'
import Haptics from './views/Haptics.svelte'
import { onMount, tick } from 'svelte'
import { ask } from '@tauri-apps/plugin-dialog'
@ -91,6 +92,11 @@
component: Shell,
icon: 'i-codicon-terminal-bash'
},
{
label: 'Opener',
component: Opener,
icon: 'i-codicon-link-external'
},
{
label: 'Store',
component: Store,
@ -130,6 +136,11 @@
label: 'Geolocation',
component: Geolocation,
icon: 'i-ph-map-pin'
},
isMobile && {
label: 'Haptics',
component: Haptics,
icon: 'i-ph-vibrate'
}
]
@ -205,7 +216,7 @@
if (consoleTextEl) consoleTextEl.scrollTop = consoleTextEl.scrollHeight
}
// this function is renders HTML without sanitizing it so it's insecure
// this function renders HTML without sanitizing it so it's insecure
// we only use it with our own input data
async function insecureRenderHtml(html) {
messages.update((r) => [
@ -334,42 +345,46 @@
children:h-100% children:w-12 children:inline-flex
children:items-center children:justify-center"
>
<span
<button
aria-label="Toggle dark mode"
title={isDark ? 'Switch to Light mode' : 'Switch to Dark mode'}
class="hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
class="bg-inherit border-none hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
on:click={toggleDark}
>
{#if isDark}
<div class="i-ph-sun" />
<div class="i-ph-sun"></div>
{:else}
<div class="i-ph-moon" />
<div class="i-ph-moon"></div>
{/if}
</span>
<span
</button>
<button
aria-label="Minimize window"
title="Minimize"
class="hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
class="bg-inherit border-none hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
on:click={minimize}
>
<div class="i-codicon-chrome-minimize" />
</span>
<span
<div class="i-codicon-chrome-minimize"></div>
</button>
<button
aria-label="Maximize window"
title={isWindowMaximized ? 'Restore' : 'Maximize'}
class="hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
class="bg-inherit border-none hover:bg-hoverOverlay active:bg-hoverOverlayDarker dark:hover:bg-darkHoverOverlay dark:active:bg-darkHoverOverlayDarker"
on:click={toggleMaximize}
>
{#if isWindowMaximized}
<div class="i-codicon-chrome-restore" />
<div class="i-codicon-chrome-restore"></div>
{:else}
<div class="i-codicon-chrome-maximize" />
<div class="i-codicon-chrome-maximize"></div>
{/if}
</span>
<span
</button>
<button
aria-label="Close window"
title="Close"
class="hover:bg-red-700 dark:hover:bg-red-700 hover:text-darkPrimaryText active:bg-red-700/90 dark:active:bg-red-700/90 active:text-darkPrimaryText"
class="bg-inherit border-none hover:bg-red-700 dark:hover:bg-red-700 hover:text-darkPrimaryText active:bg-red-700/90 dark:active:bg-red-700/90 active:text-darkPrimaryText"
on:click={close}
>
<div class="i-codicon-chrome-close" />
</span>
<div class="i-codicon-chrome-close"></div>
</button>
</span>
</div>
{/if}
@ -377,13 +392,13 @@
<!-- Sidebar toggle, only visible on small screens -->
<div
id="sidebarToggle"
class="z-2000 sidebar-toggle display-none lt-sm:flex justify-center absolute items-center w-8 h-8 rd-8
class="z-2000 sidebar-toggle hidden lt-sm:flex justify-center absolute items-center w-8 h-8 rd-8
bg-accent dark:bg-darkAccent active:bg-accentDark dark:active:bg-darkAccentDark"
>
{#if isSideBarOpen}
<span class="i-codicon-close animate-duration-300ms animate-fade-in" />
<span class="i-codicon-close animate-duration-300ms animate-fade-in"></span>
{:else}
<span class="i-codicon-menu animate-duration-300ms animate-fade-in" />
<span class="i-codicon-menu animate-duration-300ms animate-fade-in"></span>
{/if}
</div>
@ -395,24 +410,21 @@
class="lt-sm:h-screen lt-sm:shadow-lg lt-sm:shadow lt-sm:transition-transform lt-sm:absolute lt-sm:z-1999
bg-darkPrimaryLighter transition-colors-250 overflow-hidden grid select-none px-2"
>
<img
on:click={() => open('https://tauri.app/')}
class="self-center p-7 cursor-pointer"
src="tauri_logo.png"
alt="Tauri logo"
/>
<a href="https://tauri.app" target="_blank">
<img class="p-7" src="tauri_logo.png" alt="Tauri logo" />
</a>
{#if !isWindows}
<a href="##" class="nv justify-between h-8" on:click={toggleDark}>
{#if isDark}
Switch to Light mode
<div class="i-ph-sun" />
<div class="i-ph-sun"></div>
{:else}
Switch to Dark mode
<div class="i-ph-moon" />
<div class="i-ph-moon"></div>
{/if}
</a>
<br />
<div class="bg-white/5 h-2px" />
<div class="bg-white/5 h-2px"></div>
<br />
{/if}
@ -422,7 +434,7 @@
href="https://tauri.app/v1/guides/"
>
Documentation
<span class="i-codicon-link-external" />
<span class="i-codicon-link-external"></span>
</a>
<a
class="nv justify-between h-8"
@ -430,7 +442,7 @@
href="https://github.com/tauri-apps/tauri"
>
GitHub
<span class="i-codicon-link-external" />
<span class="i-codicon-link-external"></span>
</a>
<a
class="nv justify-between h-8"
@ -438,10 +450,10 @@
href="https://github.com/tauri-apps/tauri/tree/dev/examples/api"
>
Source
<span class="i-codicon-link-external" />
<span class="i-codicon-link-external"></span>
</a>
<br />
<div class="bg-white/5 h-2px" />
<div class="bg-white/5 h-2px"></div>
<br />
<div
class="flex flex-col overflow-y-auto children-h-10 children-flex-none gap-1"
@ -456,7 +468,7 @@
isSideBarOpen = false
}}
>
<div class="{view.icon} mr-2" />
<div class="{view.icon} mr-2"></div>
<p>{view.label}</p></a
>
{/if}
@ -485,21 +497,23 @@
id="console"
class="select-none h-15rem grid grid-rows-[2px_2rem_1fr] gap-1 overflow-hidden"
>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
on:mousedown={startResizingConsole}
class="bg-black/20 h-2px cursor-ns-resize"
/>
></div>
<div class="flex justify-between items-center px-2">
<p class="font-semibold">Console</p>
<div
class="cursor-pointer h-85% rd-1 p-1 flex justify-center items-center
<button
aria-label="Clear Console"
class="cursor-pointer h-85% rd-1 p-1 flex justify-center items-center border-none bg-inherit
hover:bg-hoverOverlay dark:hover:bg-darkHoverOverlay
active:bg-hoverOverlay/25 dark:active:bg-darkHoverOverlay/25
"
on:click={clear}
>
<div class="i-codicon-clear-all" />
</div>
<div class="i-codicon-clear-all"></div>
</button>
</div>
<div
bind:this={consoleTextEl}

@ -5,8 +5,9 @@
import 'uno.css'
import './app.css'
import App from './App.svelte'
import { mount } from 'svelte'
const app = new App({
const app = mount(App, {
target: document.querySelector('#app')
})

@ -1,14 +1,14 @@
<script>
import { getMatches } from "@tauri-apps/plugin-cli";
import { getMatches } from '@tauri-apps/plugin-cli'
export let onMessage;
export let onMessage
function cliMatches() {
getMatches().then(onMessage).catch(onMessage);
getMatches().then(onMessage).catch(onMessage)
}
</script>
<p>
<div>
This binary can be run from the terminal and takes the following arguments:
<code class="code-block flex flex-wrap my-2">
<pre>
@ -17,7 +17,7 @@
--verbose</pre>
</code>
Additionally, it has a <code>update --background</code> subcommand.
</p>
</div>
<br />
<div class="note">
Note that the arguments are only parsed, not implemented.

@ -0,0 +1,46 @@
<script>
import {
vibrate,
impactFeedback,
notificationFeedback,
selectionFeedback
} from '@tauri-apps/plugin-haptics'
export let onMessage
</script>
<div>
<button
class="btn"
on:click={() => vibrate(300).then(onMessage).catch(onMessage)}
>vibrate short</button
>
<button
class="btn"
on:click={() => vibrate(1500).then(onMessage).catch(onMessage)}
>vibrate long</button
>
<button
class="btn"
on:click={() => impactFeedback('medium').then(onMessage).catch(onMessage)}
>impact medium</button
>
<button
class="btn"
on:click={() =>
notificationFeedback('warning').then(onMessage).catch(onMessage)}
>notification warning</button
>
<button
class="btn"
on:click={() => selectionFeedback().then(onMessage).catch(onMessage)}
>selection</button
>
</div>
<br />
<p>
Depending on your device settings for haptic feedback some of the buttons may
not work.
</p>

@ -1,69 +1,69 @@
<script>
import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
import { JsonView } from "@zerodevx/svelte-json-view";
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
import { JsonView } from '@zerodevx/svelte-json-view'
let httpMethod = "GET";
let httpBody = "";
let httpMethod = 'GET'
let httpBody = ''
export let onMessage;
export let onMessage
async function makeHttpRequest() {
let method = httpMethod || "GET";
let method = httpMethod || 'GET'
const options = {
method: method || "GET",
headers: {},
};
method: method || 'GET',
headers: {}
}
let bodyType;
let bodyType
if (method !== "GET") {
options.body = httpBody;
if (method !== 'GET') {
options.body = httpBody
if (
(httpBody.startsWith("{") && httpBody.endsWith("}")) ||
(httpBody.startsWith("[") && httpBody.endsWith("]"))
(httpBody.startsWith('{') && httpBody.endsWith('}')) ||
(httpBody.startsWith('[') && httpBody.endsWith(']'))
) {
options.headers["Content-Type"] = "application/json";
bodyType = "json";
} else if (httpBody !== "") {
bodyType = "text";
options.headers['Content-Type'] = 'application/json'
bodyType = 'json'
} else if (httpBody !== '') {
bodyType = 'text'
}
}
const response = await tauriFetch("http://localhost:3003", options);
const response = await tauriFetch('http://localhost:3003', options)
const body =
bodyType === "json" ? await response.json() : await response.text();
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,
});
body
})
}
/// http form
let foo = "baz";
let bar = "qux";
let result = null;
let foo = 'baz'
let bar = 'qux'
let result = null
async function doPost() {
const form = new FormData();
form.append("foo", foo);
form.append("bar", bar);
const response = await tauriFetch("http://localhost:3003/tauri", {
method: "POST",
body: form,
});
const form = new FormData()
form.append('foo', foo)
form.append('bar', bar)
const response = await tauriFetch('http://localhost:3003/tauri', {
method: 'POST',
body: form
})
result = {
url: response.url,
status: response.status,
ok: response.ok,
headers: Object.fromEntries(response.headers.entries()),
body: await response.text(),
};
body: await response.text()
}
}
</script>
@ -82,7 +82,7 @@
placeholder="Request body"
rows="5"
bind:value={httpBody}
/>
></textarea>
<br />
<button class="btn" id="make-request"> Make request </button>
</form>

@ -0,0 +1,66 @@
<script>
import * as opener from '@tauri-apps/plugin-opener'
export let onMessage
let url = ''
let urlProgram = ''
function openUrl() {
opener.openUrl(url, urlProgram ? urlProgram : undefined).catch(onMessage)
}
let path = ''
let pathProgram = ''
function openPath() {
opener
.openPath(path, pathProgram ? pathProgram : undefined)
.catch(onMessage)
}
let revealPath = ''
function revealItemInDir() {
opener.revealItemInDir(revealPath).catch(onMessage)
}
</script>
<div class="flex flex-col gap-2">
<form
class="flex flex-row gap-2 items-center"
on:submit|preventDefault={openUrl}
>
<button class="btn" type="submit">Open URL</button>
<input
class="input grow"
placeholder="Type the URL to open..."
bind:value={url}
/>
<span> with </span>
<input class="input" bind:value={urlProgram} />
</form>
<form
class="flex flex-row gap-2 items-center"
on:submit|preventDefault={openPath}
>
<button class="btn" type="submit">Open Path</button>
<input
class="input grow"
placeholder="Type the path to open..."
bind:value={path}
/>
<span> with </span>
<input class="input" bind:value={pathProgram} />
</form>
<form
class="flex flex-row gap-2 items-center"
on:submit|preventDefault={revealItemInDir}
>
<button class="btn" type="submit">Reveal</button>
<input
class="input grow"
placeholder="Type the path to reveal..."
bind:value={revealPath}
/>
</form>
</div>

@ -1,38 +1,44 @@
<script>
import { scan, checkPermissions, requestPermissions, Format, cancel } from "@tauri-apps/plugin-barcode-scanner";
import {
scan,
checkPermissions,
requestPermissions,
Format,
cancel
} from '@tauri-apps/plugin-barcode-scanner'
export let onMessage;
export let onMessage
let scanning = false;
let windowed = true;
let formats = [Format.QRCode];
const supportedFormats = [Format.QRCode, Format.EAN13];
let scanning = false
let windowed = true
let formats = [Format.QRCode]
const supportedFormats = [Format.QRCode, Format.EAN13]
async function startScan() {
let permission = await checkPermissions();
let permission = await checkPermissions()
if (permission === 'prompt') {
permission = await requestPermissions();
permission = await requestPermissions()
}
if (permission === 'granted') {
scanning = true;
scanning = true
scan({ windowed, formats })
.then((res) => {
scanning = false;
onMessage(res);
scanning = false
onMessage(res)
})
.catch((error) => {
scanning = false;
onMessage(error);
});
scanning = false
onMessage(error)
})
} else {
onMessage('Permission denied')
}
}
async function cancelScan() {
await cancel();
scanning = false;
onMessage("cancelled");
await cancel()
scanning = false
onMessage('cancelled')
}
</script>
@ -59,11 +65,12 @@
<div class="barcode-scanner--area--container">
<div class="relative">
<p>Aim your camera at a QR code</p>
<button class="btn" type="button" on:click={cancelScan}>Cancel</button>
<button class="btn" type="button" on:click={cancelScan}>Cancel</button
>
</div>
<div class="square surround-cover">
<div class="barcode-scanner--area--outer surround-cover">
<div class="barcode-scanner--area--inner" />
<div class="barcode-scanner--area--inner"></div>
</div>
</div>
</div>
@ -111,7 +118,7 @@
transition: 0.3s;
}
.square:after {
content: "";
content: '';
top: 0;
display: block;
padding-bottom: 100%;
@ -141,7 +148,8 @@
width: 100%;
margin: 1rem;
border: 2px solid #fff;
box-shadow: 0px 0px 2px 1px rgb(0 0 0 / 0.5),
box-shadow:
0px 0px 2px 1px rgb(0 0 0 / 0.5),
inset 0px 0px 2px 1px rgb(0 0 0 / 0.5);
border-radius: 1rem;
}

@ -1,56 +1,56 @@
<script>
import { check } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";
import { check } from '@tauri-apps/plugin-updater'
import { relaunch } from '@tauri-apps/plugin-process'
export let onMessage;
export let onMessage
let isChecking, isInstalling, newUpdate;
let isChecking, isInstalling, newUpdate
let totalSize = 0,
downloadedSize = 0;
downloadedSize = 0
async function checkUpdate() {
isChecking = true;
isChecking = true
try {
const update = await check();
onMessage(`Should update: ${update.available}`);
onMessage(update);
const update = await check()
onMessage(`Should update: ${update.available}`)
onMessage(update)
newUpdate = update;
newUpdate = update
} catch (e) {
onMessage(e);
onMessage(e)
} finally {
isChecking = false;
isChecking = false
}
}
async function install() {
isInstalling = true;
downloadedSize = 0;
isInstalling = true
downloadedSize = 0
try {
await newUpdate.downloadAndInstall((downloadProgress) => {
switch (downloadProgress.event) {
case "Started":
totalSize = downloadProgress.data.contentLength;
break;
case "Progress":
downloadedSize += downloadProgress.data.chunkLength;
break;
case "Finished":
break;
case 'Started':
totalSize = downloadProgress.data.contentLength
break
case 'Progress':
downloadedSize += downloadProgress.data.chunkLength
break
case 'Finished':
break
}
});
onMessage("Installation complete, restarting...");
await new Promise((resolve) => setTimeout(resolve, 2000));
await relaunch();
})
onMessage('Installation complete, restarting...')
await new Promise((resolve) => setTimeout(resolve, 2000))
await relaunch()
} catch (e) {
console.error(e);
onMessage(e);
console.error(e)
onMessage(e)
} finally {
isInstalling = false;
isInstalling = false
}
}
$: progress = totalSize ? Math.round((downloadedSize / totalSize) * 100) : 0;
$: progress = totalSize ? Math.round((downloadedSize / totalSize) * 100) : 0
</script>
<div class="flex children:grow children:h10">
@ -61,7 +61,7 @@
{:else}
<div class="progress">
<span>{progress}%</span>
<div class="progress-bar" style="width: {progress}%" />
<div class="progress-bar" style="width: {progress}%"></div>
</div>
{/if}
</div>

@ -7,23 +7,24 @@
"build": "pnpm run -r --parallel --filter !plugins-workspace --filter !\"./plugins/*/examples/**\" --filter !\"./examples/*\" build",
"lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check ."
"format:check": "prettier --check .",
"example:api:dev": "pnpm run --filter \"api\" tauri dev"
},
"devDependencies": {
"@eslint/js": "9.13.0",
"@eslint/js": "9.16.0",
"@rollup/plugin-node-resolve": "15.3.0",
"@rollup/plugin-terser": "0.4.4",
"@rollup/plugin-typescript": "11.1.6",
"@types/eslint__js": "8.42.3",
"covector": "^0.12.3",
"eslint": "9.13.0",
"eslint": "9.16.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-security": "3.0.1",
"prettier": "3.3.3",
"rollup": "4.22.4",
"tslib": "2.7.0",
"typescript": "5.6.3",
"typescript-eslint": "8.10.0"
"prettier": "3.4.2",
"rollup": "4.28.1",
"tslib": "2.8.1",
"typescript": "5.7.2",
"typescript-eslint": "8.17.0"
},
"resolutions": {
"semver": ">=7.5.2",

@ -27,6 +27,5 @@ tauri-plugin = { workspace = true, features = ["build"] }
serde = { workspace = true }
serde_json = { workspace = true }
tauri = { workspace = true }
log = { workspace = true }
thiserror = { workspace = true }
auto-launch = "0.5"

@ -54,7 +54,7 @@ yarn add https://github.com/tauri-apps/tauri-plugin-autostart#v2
First you need to register the core plugin with Tauri:
`src-tauri/src/main.rs`
`src-tauri/src/lib.rs`
```rust
use tauri_plugin_autostart::MacosLauncher;

@ -2,8 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
// 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(
@ -13,8 +11,6 @@
#![cfg(not(any(target_os = "android", target_os = "ios")))]
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
#[cfg(target_os = "macos")]
use log::info;
use serde::{ser::Serializer, Serialize};
use tauri::{
command,
@ -135,7 +131,6 @@ pub fn init<R: Runtime>(
} else {
exe_path
};
info!("auto_start path {}", &app_path);
builder.set_app_path(&app_path);
}
#[cfg(target_os = "linux")]

@ -54,7 +54,7 @@ yarn add https://github.com/tauri-apps/tauri-plugin-barcode-scanner#v2
First you need to register the core plugin with Tauri:
`src-tauri/src/main.rs`
`src-tauri/src/lib.rs`
```rust
fn main() {

@ -56,7 +56,7 @@ yarn add https://github.com/tauri-apps/tauri-plugin-biometric#v2
First you need to register the core plugin with Tauri:
`src-tauri/src/main.rs`
`src-tauri/src/lib.rs`
```rust
fn main() {

@ -55,7 +55,7 @@ yarn add https://github.com/tauri-apps/tauri-plugin-cli#v2
First you need to register the core plugin with Tauri:
`src-tauri/src/main.rs`
`src-tauri/src/lib.rs`
```rust
fn main() {

@ -2,8 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
// 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.

@ -2,6 +2,14 @@
## \[2.0.1]
- [`3fa0fc09`](https://github.com/tauri-apps/plugins-workspace/commit/3fa0fc09bbee0d619801e5757af9fb3c09883c97) ([#2099](https://github.com/tauri-apps/plugins-workspace/pull/2099) by [@rasteiner](https://github.com/tauri-apps/plugins-workspace/../../rasteiner)) Fix clipboard manager client side api not copying fallback alternative text when calling `writeHtml`.
## \[2.0.2]
- [`d57df4de`](https://github.com/tauri-apps/plugins-workspace/commit/d57df4debe7c75cfbd6d6558fff1beb07dbee54c) ([#1986](https://github.com/tauri-apps/plugins-workspace/pull/1986) by [@RikaKagurasaka](https://github.com/tauri-apps/plugins-workspace/../../RikaKagurasaka)) Fix that `read_image` wrongly set the image rgba data with binary PNG data.
## \[2.0.1]
- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
## \[2.0.0]

@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-clipboard-manager"
version = "2.0.1"
version = "2.0.2"
description = "Read and write to the system clipboard."
edition = { workspace = true }
authors = { workspace = true }
@ -37,4 +37,3 @@ tauri = { workspace = true, features = ["wry"] }
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
arboard = "3"
image = "0.25"

@ -54,7 +54,7 @@ yarn add https://github.com/tauri-apps/tauri-plugin-clipboard-manager#v2
First you need to register the core plugin with Tauri:
`src-tauri/src/main.rs`
`src-tauri/src/lib.rs`
```rust
fn main() {

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_CLIPBOARD_MANAGER__=function(e){"use strict";var t;async function r(e,t={},r){return window.__TAURI_INTERNALS__.invoke(e,t,r)}"function"==typeof SuppressedError&&SuppressedError;class n{get rid(){return function(e,t,r,n){if("a"===r&&!n)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!n:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?n:"a"===r?n.call(e):n?n.value:t.get(e)}(this,t,"f")}constructor(e){t.set(this,void 0),function(e,t,r,n,a){if("function"==typeof t?e!==t||!a:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");t.set(e,r)}(this,t,e)}async close(){return r("plugin:resources|close",{rid:this.rid})}}t=new WeakMap;class a extends n{constructor(e){super(e)}static async new(e,t,n){return r("plugin:image|new",{rgba:i(e),width:t,height:n}).then((e=>new a(e)))}static async fromBytes(e){return r("plugin:image|from_bytes",{bytes:i(e)}).then((e=>new a(e)))}static async fromPath(e){return r("plugin:image|from_path",{path:e}).then((e=>new a(e)))}async rgba(){return r("plugin:image|rgba",{rid:this.rid}).then((e=>new Uint8Array(e)))}async size(){return r("plugin:image|size",{rid:this.rid})}}function i(e){return null==e?null:"string"==typeof e?e:e instanceof a?e.rid:e}return e.clear=async function(){await r("plugin:clipboard-manager|clear")},e.readImage=async function(){return await r("plugin:clipboard-manager|read_image").then((e=>new a(e)))},e.readText=async function(){return await r("plugin:clipboard-manager|read_text")},e.writeHtml=async function(e,t){await r("plugin:clipboard-manager|write_html",{html:e,altHtml:t})},e.writeImage=async function(e){await r("plugin:clipboard-manager|write_image",{image:i(e)})},e.writeText=async function(e,t){await r("plugin:clipboard-manager|write_text",{label:t?.label,text:e})},e}({});Object.defineProperty(window.__TAURI__,"clipboardManager",{value:__TAURI_PLUGIN_CLIPBOARD_MANAGER__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_CLIPBOARD_MANAGER__=function(e){"use strict";var t;async function r(e,t={},r){return window.__TAURI_INTERNALS__.invoke(e,t,r)}"function"==typeof SuppressedError&&SuppressedError;class n{get rid(){return function(e,t,r,n){if("a"===r&&!n)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof t?e!==t||!n:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?n:"a"===r?n.call(e):n?n.value:t.get(e)}(this,t,"f")}constructor(e){t.set(this,void 0),function(e,t,r,n,a){if("function"==typeof t?e!==t||!a:!t.has(e))throw new TypeError("Cannot write private member to an object whose class did not declare it");t.set(e,r)}(this,t,e)}async close(){return r("plugin:resources|close",{rid:this.rid})}}t=new WeakMap;class a extends n{constructor(e){super(e)}static async new(e,t,n){return r("plugin:image|new",{rgba:i(e),width:t,height:n}).then((e=>new a(e)))}static async fromBytes(e){return r("plugin:image|from_bytes",{bytes:i(e)}).then((e=>new a(e)))}static async fromPath(e){return r("plugin:image|from_path",{path:e}).then((e=>new a(e)))}async rgba(){return r("plugin:image|rgba",{rid:this.rid}).then((e=>new Uint8Array(e)))}async size(){return r("plugin:image|size",{rid:this.rid})}}function i(e){return null==e?null:"string"==typeof e?e:e instanceof a?e.rid:e}return e.clear=async function(){await r("plugin:clipboard-manager|clear")},e.readImage=async function(){return await r("plugin:clipboard-manager|read_image").then((e=>new a(e)))},e.readText=async function(){return await r("plugin:clipboard-manager|read_text")},e.writeHtml=async function(e,t){await r("plugin:clipboard-manager|write_html",{html:e,altText:t})},e.writeImage=async function(e){await r("plugin:clipboard-manager|write_image",{image:i(e)})},e.writeText=async function(e,t){await r("plugin:clipboard-manager|write_text",{label:t?.label,text:e})},e}({});Object.defineProperty(window.__TAURI__,"clipboardManager",{value:__TAURI_PLUGIN_CLIPBOARD_MANAGER__})}

@ -65,6 +65,7 @@ async function readText(): Promise<string> {
* 0, 255, 0, 255,
* ];
* await writeImage(buffer);
* ```
*
* @returns A promise indicating the success or failure of the operation.
*
@ -90,7 +91,7 @@ async function writeImage(
* import { readImage } from '@tauri-apps/plugin-clipboard-manager';
*
* const clipboardImage = await readImage();
* const blob = new Blob([clipboardImage.bytes], { type: 'image' })
* const blob = new Blob([await clipboardImage.rbga()], { type: 'image' })
* const url = URL.createObjectURL(blob)
* ```
* @since 2.0.0
@ -120,10 +121,10 @@ async function readImage(): Promise<Image> {
*
* @since 2.0.0
*/
async function writeHtml(html: string, altHtml?: string): Promise<void> {
async function writeHtml(html: string, altText?: string): Promise<void> {
await invoke('plugin:clipboard-manager|write_html', {
html,
altHtml
altText
})
}

@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-clipboard-manager",
"version": "2.0.0",
"version": "2.0.1",
"license": "MIT OR Apache-2.0",
"authors": [
"Tauri Programme within The Commons Conservancy"

@ -3,7 +3,6 @@
// SPDX-License-Identifier: MIT
use arboard::ImageData;
use image::ImageEncoder;
use serde::de::DeserializeOwned;
use tauri::{image::Image, plugin::PluginApi, AppHandle, Runtime};
@ -85,16 +84,11 @@ impl<R: Runtime> Clipboard<R> {
match &self.clipboard {
Ok(clipboard) => {
let image = clipboard.lock().unwrap().get_image()?;
let mut buffer: Vec<u8> = Vec::new();
image::codecs::png::PngEncoder::new(&mut buffer).write_image(
&image.bytes,
let image = Image::new_owned(
image.bytes.to_vec(),
image.width as u32,
image.height as u32,
image::ExtendedColorType::Rgba8,
)?;
let image = Image::new_owned(buffer, image.width as u32, image.height as u32);
);
Ok(image)
}
Err(e) => Err(crate::Error::Clipboard(e.to_string())),

@ -15,9 +15,6 @@ pub enum Error {
Clipboard(String),
#[error(transparent)]
Tauri(#[from] tauri::Error),
#[cfg(desktop)]
#[error("invalid image: {0}")]
Image(#[from] image::ImageError),
}
impl Serialize for Error {

@ -2,8 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
// 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(

@ -2,6 +2,10 @@
## \[2.0.1]
- [`b2aea045`](https://github.com/tauri-apps/plugins-workspace/commit/b2aea0456799775a7243706fdd7a5abf9a193992) ([#2008](https://github.com/tauri-apps/plugins-workspace/pull/2008) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) `onOpenUrl()` will now not call `getCurrent()` anymore, matching the documented behavior.
## \[2.0.1]
- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
## \[2.0.0]

@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-deep-link"
version = "2.0.1"
version = "2.0.2"
description = "Set your Tauri application as the default handler for an URL"
authors = { workspace = true }
license = { workspace = true }
@ -32,7 +32,7 @@ serde = { workspace = true }
serde_json = { workspace = true }
tauri = { workspace = true }
tauri-utils = { workspace = true }
log = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }

@ -133,7 +133,7 @@ Under `tauri.conf.json > plugins > deep-link`, configure the domains (mobile) an
First you need to register the core plugin with Tauri:
`src-tauri/src/main.rs`
`src-tauri/src/lib.rs`
```rust
fn main() {

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_DEEP_LINK__=function(e){"use strict";function n(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}async function r(e,n={},r){return window.__TAURI_INTERNALS__.invoke(e,n,r)}var t;async function i(e,t,i){const a={kind:"Any"};return r("plugin:event|listen",{event:e,target:a,handler:n(t)}).then((n=>async()=>async function(e,n){await r("plugin:event|unlisten",{event:e,eventId:n})}(e,n)))}async function a(){return await r("plugin:deep-link|get_current")}return"function"==typeof SuppressedError&&SuppressedError,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG_ENTER="tauri://drag-enter",e.DRAG_OVER="tauri://drag-over",e.DRAG_DROP="tauri://drag-drop",e.DRAG_LEAVE="tauri://drag-leave"}(t||(t={})),e.getCurrent=a,e.isRegistered=async function(e){return await r("plugin:deep-link|is_registered",{protocol:e})},e.onOpenUrl=async function(e){const n=await a();return n&&e(n),await i("deep-link://new-url",(n=>{e(n.payload)}))},e.register=async function(e){return await r("plugin:deep-link|register",{protocol:e})},e.unregister=async function(e){return await r("plugin:deep-link|unregister",{protocol:e})},e}({});Object.defineProperty(window.__TAURI__,"deepLink",{value:__TAURI_PLUGIN_DEEP_LINK__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_DEEP_LINK__=function(e){"use strict";function n(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}async function r(e,n={},r){return window.__TAURI_INTERNALS__.invoke(e,n,r)}var t;async function i(e,t,i){const a={kind:"Any"};return r("plugin:event|listen",{event:e,target:a,handler:n(t)}).then((n=>async()=>async function(e,n){await r("plugin:event|unlisten",{event:e,eventId:n})}(e,n)))}return"function"==typeof SuppressedError&&SuppressedError,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG_ENTER="tauri://drag-enter",e.DRAG_OVER="tauri://drag-over",e.DRAG_DROP="tauri://drag-drop",e.DRAG_LEAVE="tauri://drag-leave"}(t||(t={})),e.getCurrent=async function(){return await r("plugin:deep-link|get_current")},e.isRegistered=async function(e){return await r("plugin:deep-link|is_registered",{protocol:e})},e.onOpenUrl=async function(e){return await i("deep-link://new-url",(n=>{e(n.payload)}))},e.register=async function(e){return await r("plugin:deep-link|register",{protocol:e})},e.unregister=async function(e){return await r("plugin:deep-link|unregister",{protocol:e})},e}({});Object.defineProperty(window.__TAURI__,"deepLink",{value:__TAURI_PLUGIN_DEEP_LINK__})}

@ -1,5 +1,11 @@
# Changelog
## \[2.0.1]
### Dependencies
- Upgraded to `deep-link-js@2.0.1`
## \[2.0.0]
- [`e2c4dfb6`](https://github.com/tauri-apps/plugins-workspace/commit/e2c4dfb6af43e5dd8d9ceba232c315f5febd55c1) Update to tauri v2 stable release.

@ -1,7 +1,7 @@
{
"name": "deep-link-example",
"private": true,
"version": "2.0.0",
"version": "2.0.1",
"type": "module",
"scripts": {
"dev": "vite",
@ -10,12 +10,12 @@
"tauri": "tauri"
},
"dependencies": {
"@tauri-apps/api": "2.0.2",
"@tauri-apps/plugin-deep-link": "2.0.0"
"@tauri-apps/api": "2.1.1",
"@tauri-apps/plugin-deep-link": "2.0.1"
},
"devDependencies": {
"@tauri-apps/cli": "2.0.3",
"@tauri-apps/cli": "2.1.0",
"typescript": "^5.2.2",
"vite": "^5.4.7"
"vite": "^6.0.0"
}
}

@ -73,7 +73,7 @@ export async function unregister(protocol: string): Promise<null> {
* await isRegistered("my-scheme");
* ```
*
* #### - **macOS / Android / iOS**: Unsupported, always returns `true`.
* #### - **macOS / Android / iOS**: Unsupported.
*
* @since 2.0.0
*/
@ -92,18 +92,13 @@ export async function isRegistered(protocol: string): Promise<boolean> {
* await onOpenUrl((urls) => { console.log(urls) });
* ```
*
* #### - **Windows / Linux**: Unsupported, the OS will spawn a new app instance passing the URL as a CLI argument.
* #### - **Windows / Linux**: Unsupported without the single-instance plugin. The OS will spawn a new app instance passing the URL as a CLI argument.
*
* @since 2.0.0
*/
export async function onOpenUrl(
handler: (urls: string[]) => void
): Promise<UnlistenFn> {
const current = await getCurrent()
if (current) {
handler(current)
}
return await listen<string[]>('deep-link://new-url', (event) => {
handler(event.payload)
})

@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-deep-link",
"version": "2.0.0",
"version": "2.0.1",
"description": "Set your Tauri application as the default handler for an URL",
"license": "MIT OR Apache-2.0",
"authors": [

@ -2,8 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::sync::Arc;
use tauri::{
plugin::{Builder, PluginApi, TauriPlugin},
AppHandle, EventId, Listener, Manager, Runtime,
@ -217,7 +215,7 @@ mod imp {
current.replace(vec![url.clone()]);
let _ = self.app.emit("deep-link://new-url", vec![url]);
} else if cfg!(debug_assertions) {
log::warn!("argument {url} does not match any configured deep link scheme; skipping it");
tracing::warn!("argument {url} does not match any configured deep link scheme; skipping it");
}
}
}
@ -478,13 +476,10 @@ impl OpenUrlEvent {
}
impl<R: Runtime> DeepLink<R> {
/// Handle a new deep link being triggered to open the app.
/// Helper function for the `deep-link://new-url` event to run a function each time the protocol is triggered while the app is running.
///
/// To avoid race conditions, if the app was started with a deep link,
/// the closure gets immediately called with the deep link URL.
/// Use `get_current` on app load to check whether your app was started via a deep link.
pub fn on_open_url<F: Fn(OpenUrlEvent) + Send + Sync + 'static>(&self, f: F) -> EventId {
let f = Arc::new(f);
let f_ = f.clone();
let event_id = self.app.listen("deep-link://new-url", move |event| {
if let Ok(urls) = serde_json::from_str(event.payload()) {
f(OpenUrlEvent {
@ -494,13 +489,6 @@ impl<R: Runtime> DeepLink<R> {
}
});
if let Ok(Some(current)) = self.get_current() {
f_(OpenUrlEvent {
id: event_id,
urls: current,
})
}
event_id
}
}

@ -1,5 +1,13 @@
# Changelog
## \[2.0.4]
- [`76f99ce9`](https://github.com/tauri-apps/plugins-workspace/commit/76f99ce999a2ff9e40235c1675e3eb6570b5e1e2) ([#2108](https://github.com/tauri-apps/plugins-workspace/pull/2108) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) The `Dialog` struct is now correctly exported, primarily to fix the documentation on `docs.rs`.
### Dependencies
- Upgraded to `fs@2.1.0`
## \[2.0.3]
### Dependencies

@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-dialog"
version = "2.0.3"
version = "2.0.4"
description = "Native system dialogs for opening and saving files along with message dialogs on your Tauri application."
edition = { workspace = true }
authors = { workspace = true }
@ -34,7 +34,7 @@ tauri = { workspace = true }
log = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }
tauri-plugin-fs = { path = "../fs", version = "2.0.3" }
tauri-plugin-fs = { path = "../fs", version = "2.1.0" }
[target.'cfg(target_os = "ios")'.dependencies]
tauri = { workspace = true, features = ["wry"] }

@ -54,7 +54,7 @@ yarn add https://github.com/tauri-apps/tauri-plugin-dialog#v2
First you need to register the core plugin with Tauri:
`src-tauri/src/main.rs`
`src-tauri/src/lib.rs`
```rust
fn main() {

@ -143,7 +143,7 @@ pub(crate) async fn open<R: Runtime>(
for folder in folders {
if let Ok(path) = folder.clone().into_path() {
if let Some(s) = window.try_fs_scope() {
s.allow_directory(&path, options.recursive);
s.allow_directory(&path, options.recursive)?;
}
tauri_scope.allow_directory(&path, options.directory)?;
}
@ -157,7 +157,7 @@ pub(crate) async fn open<R: Runtime>(
if let Some(folder) = &folder {
if let Ok(path) = folder.clone().into_path() {
if let Some(s) = window.try_fs_scope() {
s.allow_directory(&path, options.recursive);
s.allow_directory(&path, options.recursive)?;
}
tauri_scope.allow_directory(&path, options.directory)?;
}
@ -175,7 +175,7 @@ pub(crate) async fn open<R: Runtime>(
for file in files {
if let Ok(path) = file.clone().into_path() {
if let Some(s) = window.try_fs_scope() {
s.allow_file(&path);
s.allow_file(&path)?;
}
tauri_scope.allow_file(&path)?;
@ -190,7 +190,7 @@ pub(crate) async fn open<R: Runtime>(
if let Some(file) = &file {
if let Ok(path) = file.clone().into_path() {
if let Some(s) = window.try_fs_scope() {
s.allow_file(&path);
s.allow_file(&path)?;
}
tauri_scope.allow_file(&path)?;
}
@ -232,7 +232,7 @@ pub(crate) async fn save<R: Runtime>(
if let Some(p) = &path {
if let Ok(path) = p.clone().into_path() {
if let Some(s) = window.try_fs_scope() {
s.allow_file(&path);
s.allow_file(&path)?;
}
tauri_scope.allow_file(&path)?;
}

@ -2,8 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
// 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(
@ -41,6 +39,11 @@ use desktop::*;
#[cfg(mobile)]
use mobile::*;
#[cfg(desktop)]
pub use desktop::Dialog;
#[cfg(mobile)]
pub use mobile::Dialog;
pub(crate) const OK: &str = "Ok";
pub(crate) const CANCEL: &str = "Cancel";
pub(crate) const YES: &str = "Yes";

@ -2,6 +2,16 @@
## \[2.0.3]
- [`ed981027`](https://github.com/tauri-apps/plugins-workspace/commit/ed981027dd4fba7d0e2f836eb5db34d344388d73) ([#1962](https://github.com/tauri-apps/plugins-workspace/pull/1962) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Improve performance of `readTextFile` and `readTextFileLines` APIs
- [`3e78173d`](https://github.com/tauri-apps/plugins-workspace/commit/3e78173df9ce90aa3b19e1f36d1f8712c5020fb6) ([#2018](https://github.com/tauri-apps/plugins-workspace/pull/2018) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Fix `readDir` function failing to read directories that contain broken symlinks.
- [`5092ea5e`](https://github.com/tauri-apps/plugins-workspace/commit/5092ea5e89817c0550d09b0a4ad17bf1253b23df) ([#1964](https://github.com/tauri-apps/plugins-workspace/pull/1964) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Add support for using `ReadableStream<Unit8Array>` with `writeFile` API.
## \[2.0.2]
- [`77149dc4`](https://github.com/tauri-apps/plugins-workspace/commit/77149dc4320d26b413e4a6bbe82c654367c51b32) ([#1965](https://github.com/tauri-apps/plugins-workspace/pull/1965) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Fix `writeTextFile` converting UTF-8 characters (for example `äöü`) in the given path into replacement character (`<60>`)
## \[2.0.3]
- [`14cee64c`](https://github.com/tauri-apps/plugins-workspace/commit/14cee64c82a72655ae6a4ac0892736a2959dbda5) ([#1958](https://github.com/tauri-apps/plugins-workspace/pull/1958) by [@bWanShiTong](https://github.com/tauri-apps/plugins-workspace/../../bWanShiTong)) Fix compilation on targets with pointer width of `16` or `32`
## \[2.0.1]
@ -187,3 +197,11 @@
ac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
apps/plugins-workspace/pull/371)) First v2 alpha release!
.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
kspace/pull/371)) First v2 alpha release!
s/plugins-workspace/pull/371)) First v2 alpha release!
ac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!
717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release!

@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-fs"
version = "2.0.3"
version = "2.1.0"
description = "Access the file system."
authors = { workspace = true }
license = { workspace = true }
@ -14,7 +14,7 @@ rustc-args = ["--cfg", "docsrs"]
rustdoc-args = ["--cfg", "docsrs"]
[package.metadata.platforms.support]
windows = { level = "full", notes = "" }
windows = { level = "full", notes = "Apps installed via MSI or NSIS in `perMachine` and `both` mode require admin permissions for write acces in `$RESOURCES` folder" }
linux = { level = "full", notes = "No write access to `$RESOURCES` folder" }
macos = { level = "full", notes = "No write access to `$RESOURCES` folder" }
android = { level = "partial", notes = "Access is restricted to Application folder by default" }
@ -24,6 +24,8 @@ ios = { level = "partial", notes = "Access is restricted to Application folder b
tauri-plugin = { workspace = true, features = ["build"] }
schemars = { workspace = true }
serde = { workspace = true }
toml = "0.8"
tauri-utils = { workspace = true, features = ["build"] }
[dependencies]
serde = { workspace = true }
@ -34,9 +36,13 @@ thiserror = { workspace = true }
url = { workspace = true }
anyhow = "1"
uuid = { version = "1", features = ["v4"] }
glob = "0.3"
notify = { version = "6", optional = true, features = ["serde"] }
notify-debouncer-full = { version = "0.3", optional = true }
glob = { workspace = true }
# TODO: Remove `serialization-compat-6` in v3
notify = { version = "7", optional = true, features = [
"serde",
"serialization-compat-6",
] }
notify-debouncer-full = { version = "0.4", optional = true }
dunce = { workspace = true }
percent-encoding = "2"

@ -54,7 +54,7 @@ yarn add https://github.com/tauri-apps/tauri-plugin-fs#v2
First you need to register the core plugin with Tauri:
`src-tauri/src/main.rs`
`src-tauri/src/lib.rs`
```rust
fn main() {

File diff suppressed because one or more lines are too long

@ -7,6 +7,8 @@ use std::{
path::{Path, PathBuf},
};
use tauri_utils::acl::manifest::PermissionFile;
#[path = "src/scope.rs"]
#[allow(dead_code)]
mod scope;
@ -16,10 +18,23 @@ mod scope;
#[serde(untagged)]
#[allow(unused)]
enum FsScopeEntry {
/// FS scope path.
/// A path that can be accessed by the webview when using the fs APIs.
/// FS scope path pattern.
///
/// The pattern can start with a variable that resolves to a system base directory.
/// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,
/// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,
/// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,
/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
Value(PathBuf),
Object {
/// FS scope path.
/// A path that can be accessed by the webview when using the fs APIs.
///
/// The pattern can start with a variable that resolves to a system base directory.
/// The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,
/// `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,
/// `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,
/// `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.
path: PathBuf,
},
}
@ -62,31 +77,32 @@ const BASE_DIR_VARS: &[&str] = &[
"APPCACHE",
"APPLOG",
];
const COMMANDS: &[&str] = &[
"mkdir",
"create",
"copy_file",
"remove",
"rename",
"truncate",
"ftruncate",
"write",
"write_file",
"write_text_file",
"read_dir",
"read_file",
"read",
"open",
"read_text_file",
"read_text_file_lines",
"read_text_file_lines_next",
"seek",
"stat",
"lstat",
"fstat",
"exists",
"watch",
"unwatch",
const COMMANDS: &[(&str, &[&str])] = &[
("mkdir", &[]),
("create", &[]),
("copy_file", &[]),
("remove", &[]),
("rename", &[]),
("truncate", &[]),
("ftruncate", &[]),
("write", &[]),
("write_file", &["open", "write"]),
("write_text_file", &[]),
("read_dir", &[]),
("read_file", &[]),
("read", &[]),
("open", &[]),
("read_text_file", &[]),
("read_text_file_lines", &["read_text_file_lines_next"]),
("read_text_file_lines_next", &[]),
("seek", &[]),
("stat", &[]),
("lstat", &[]),
("fstat", &[]),
("exists", &[]),
("watch", &[]),
("unwatch", &[]),
("size", &[]),
];
fn main() {
@ -192,9 +208,47 @@ permissions = [
}
}
tauri_plugin::Builder::new(COMMANDS)
tauri_plugin::Builder::new(&COMMANDS.iter().map(|c| c.0).collect::<Vec<_>>())
.global_api_script_path("./api-iife.js")
.global_scope_schema(schemars::schema_for!(FsScopeEntry))
.android_path("android")
.build();
// workaround to include nested permissions as `tauri_plugin` doesn't support it
let permissions_dir = autogenerated.join("commands");
for (command, nested_commands) in COMMANDS {
if nested_commands.is_empty() {
continue;
}
let permission_path = permissions_dir.join(format!("{command}.toml"));
let content = std::fs::read_to_string(&permission_path)
.unwrap_or_else(|_| panic!("failed to read {command}.toml"));
let mut permission_file = toml::from_str::<PermissionFile>(&content)
.unwrap_or_else(|_| panic!("failed to deserialize {command}.toml"));
for p in permission_file
.permission
.iter_mut()
.filter(|p| p.identifier.starts_with("allow"))
{
p.commands
.allow
.extend(nested_commands.iter().map(|s| s.to_string()));
}
let out = toml::to_string_pretty(&permission_file)
.unwrap_or_else(|_| panic!("failed to serialize {command}.toml"));
let out = format!(
r#"# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
{out}"#
);
std::fs::write(permission_path, out)
.unwrap_or_else(|_| panic!("failed to write {command}.toml"));
}
}

@ -14,16 +14,29 @@
*
* The API has a scope configuration that forces you to restrict the paths that can be accessed using glob patterns.
*
* The scope configuration is an array of glob patterns describing folder paths that are allowed.
* For instance, this scope configuration only allows accessing files on the
* *databases* folder of the {@link https://v2.tauri.app/reference/javascript/api/namespacepath/#appdatadir | `$APPDATA` directory}:
* The scope configuration is an array of glob patterns describing file/directory paths that are allowed.
* For instance, this scope configuration allows **all** enabled `fs` APIs to (only) access files in the
* *databases* directory of the {@link https://v2.tauri.app/reference/javascript/api/namespacepath/#appdatadir | `$APPDATA` directory}:
* ```json
* {
* "plugins": {
* "fs": {
* "scope": ["$APPDATA/databases/*"]
* "permissions": [
* {
* "identifier": "fs:scope",
* "allow": [{ "path": "$APPDATA/databases/*" }]
* }
* ]
* }
* ```
*
* Scopes can also be applied to specific `fs` APIs by using the API's identifier instead of `fs:scope`:
* ```json
* {
* "permissions": [
* {
* "identifier": "fs:allow-exists",
* "allow": [{ "path": "$APPDATA/databases/*" }]
* }
* ]
* }
* ```
*
@ -56,8 +69,6 @@
*
* Trying to execute any API with a URL not configured on the scope results in a promise rejection due to denied access.
*
* Note that this scope applies to **all** APIs on this module.
*
* @module
*/
@ -255,6 +266,7 @@ function fromBytes(buffer: FixedSizeArray<number, 8>): number {
const size = bytes.byteLength
let x = 0
for (let i = 0; i < size; i++) {
// eslint-disable-next-line security/detect-object-injection
const byte = bytes[i]
x *= 0x100
x += byte
@ -416,11 +428,11 @@ class FileHandle extends Resource {
}
/**
* Writes `p.byteLength` bytes from `p` to the underlying data stream. It
* resolves to the number of bytes written from `p` (`0` <= `n` <=
* `p.byteLength`) or reject with the error encountered that caused the
* Writes `data.byteLength` bytes from `data` to the underlying data stream. It
* resolves to the number of bytes written from `data` (`0` <= `n` <=
* `data.byteLength`) or reject with the error encountered that caused the
* write to stop early. `write()` must reject with a non-null error if
* would resolve to `n` < `p.byteLength`. `write()` must not modify the
* would resolve to `n` < `data.byteLength`. `write()` must not modify the
* slice data, even temporarily.
*
* @example
@ -758,10 +770,14 @@ async function readTextFile(
throw new TypeError('Must be a file URL.')
}
return await invoke<string>('plugin:fs|read_text_file', {
const arr = await invoke<ArrayBuffer | number[]>('plugin:fs|read_text_file', {
path: path instanceof URL ? path.toString() : path,
options
})
const bytes = arr instanceof ArrayBuffer ? arr : Uint8Array.from(arr)
return new TextDecoder().decode(bytes)
}
/**
@ -792,6 +808,7 @@ async function readTextFileLines(
return await Promise.resolve({
path: pathStr,
rid: null as number | null,
async next(): Promise<IteratorResult<string>> {
if (this.rid === null) {
this.rid = await invoke<number>('plugin:fs|read_text_file_lines', {
@ -800,19 +817,35 @@ async function readTextFileLines(
})
}
const [line, done] = await invoke<[string | null, boolean]>(
const arr = await invoke<ArrayBuffer | number[]>(
'plugin:fs|read_text_file_lines_next',
{ rid: this.rid }
)
// an iteration is over, reset rid for next iteration
if (done) this.rid = null
const bytes =
arr instanceof ArrayBuffer ? new Uint8Array(arr) : Uint8Array.from(arr)
// Rust side will never return an empty array for this command and
// ensure there is at least one elements there.
//
// This is an optimization to include whether we finished iteration or not (1 or 0)
// at the end of returned array to avoid serialization overhead of separate values.
const done = bytes[bytes.byteLength - 1] === 1
if (done) {
// a full iteration is over, reset rid for next iteration
this.rid = null
return { value: null, done }
}
const line = new TextDecoder().decode(bytes.slice(0, bytes.byteLength))
return {
value: done ? '' : line!,
value: line,
done
}
},
[Symbol.asyncIterator](): AsyncIterableIterator<string> {
return this
}
@ -1033,19 +1066,27 @@ interface WriteFileOptions {
*/
async function writeFile(
path: string | URL,
data: Uint8Array,
data: Uint8Array | ReadableStream<Uint8Array>,
options?: WriteFileOptions
): Promise<void> {
if (path instanceof URL && path.protocol !== 'file:') {
throw new TypeError('Must be a file URL.')
}
if (data instanceof ReadableStream) {
const file = await open(path, options)
for await (const chunk of data) {
await file.write(chunk)
}
await file.close()
} else {
await invoke('plugin:fs|write_file', data, {
headers: {
path: encodeURIComponent(path instanceof URL ? path.toString() : path),
options: JSON.stringify(options)
}
})
}
}
/**
@ -1281,6 +1322,31 @@ async function watchImmediate(
}
}
/**
* Get the size of a file or directory. For files, the `stat` functions can be used as well.
*
* If `path` is a directory, this function will recursively iterate over every file and every directory inside of `path` and therefore will be very time consuming if used on larger directories.
*
* @example
* ```typescript
* import { size, BaseDirectory } from '@tauri-apps/plugin-fs';
* // Get the size of the `$APPDATA/tauri` directory.
* const dirSize = await size('tauri', { baseDir: BaseDirectory.AppData });
* console.log(dirSize); // 1024
* ```
*
* @since 2.1.0
*/
async function size(path: string | URL): Promise<number> {
if (path instanceof URL && path.protocol !== 'file:') {
throw new TypeError('Must be a file URL.')
}
return await invoke('plugin:fs|size', {
path: path instanceof URL ? path.toString() : path
})
}
export type {
CreateOptions,
OpenOptions,
@ -1328,5 +1394,6 @@ export {
writeTextFile,
exists,
watch,
watchImmediate
watchImmediate,
size
}

@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-fs",
"version": "2.0.1",
"version": "2.0.3",
"description": "Access the file system.",
"license": "MIT OR Apache-2.0",
"authors": [

@ -5,9 +5,18 @@
[[permission]]
identifier = "allow-read-text-file-lines"
description = "Enables the read_text_file_lines command without any pre-configured scope."
commands.allow = ["read_text_file_lines"]
[permission.commands]
allow = [
"read_text_file_lines",
"read_text_file_lines_next",
]
deny = []
[[permission]]
identifier = "deny-read-text-file-lines"
description = "Denies the read_text_file_lines command without any pre-configured scope."
commands.deny = ["read_text_file_lines"]
[permission.commands]
allow = []
deny = ["read_text_file_lines"]

@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-size"
description = "Enables the size command without any pre-configured scope."
commands.allow = ["size"]
[[permission]]
identifier = "deny-size"
description = "Denies the size command without any pre-configured scope."
commands.deny = ["size"]

@ -5,9 +5,19 @@
[[permission]]
identifier = "allow-write-file"
description = "Enables the write_file command without any pre-configured scope."
commands.allow = ["write_file"]
[permission.commands]
allow = [
"write_file",
"open",
"write",
]
deny = []
[[permission]]
identifier = "deny-write-file"
description = "Denies the write_file command without any pre-configured scope."
commands.deny = ["write_file"]
[permission.commands]
allow = []
deny = ["write_file"]

@ -24,6 +24,7 @@ This default permission set prevents access to critical components
of the Tauri application by default.
On Windows the webview data folder access is denied.
#### Included permissions within this default permission set:
- `create-app-specific-dirs`
@ -3409,6 +3410,32 @@ Denies the seek command without any pre-configured scope.
<tr>
<td>
`fs:allow-size`
</td>
<td>
Enables the size command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`fs:deny-size`
</td>
<td>
Denies the size command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`fs:allow-stat`
</td>

@ -26,6 +26,7 @@ This default permission set prevents access to critical components
of the Tauri application by default.
On Windows the webview data folder access is denied.
#### Included permissions within this default permission set:
"""
permissions = [
"create-app-specific-dirs",

@ -3,4 +3,4 @@
[[permission]]
identifier = "read-meta"
description = "This enables all index or metadata related commands without any pre-configured accessible paths."
commands.allow = ["read_dir", "stat", "lstat", "fstat", "exists"]
commands.allow = ["read_dir", "stat", "lstat", "fstat", "exists", "size"]

@ -1589,6 +1589,16 @@
"type": "string",
"const": "deny-seek"
},
{
"description": "Enables the size command without any pre-configured scope.",
"type": "string",
"const": "allow-size"
},
{
"description": "Denies the size command without any pre-configured scope.",
"type": "string",
"const": "deny-size"
},
{
"description": "Enables the stat command without any pre-configured scope.",
"type": "string",
@ -1665,7 +1675,7 @@
"const": "create-app-specific-dirs"
},
{
"description": "This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n",
"description": "This set of permissions describes the what kind of\nfile system access the `fs` plugin has enabled or denied by default.\n\n#### Granted Permissions\n\nThis default permission set enables read access to the\napplication specific directories (AppConfig, AppData, AppLocalData, AppCache,\nAppLog) and all files and sub directories created in it.\nThe location of these directories depends on the operating system,\nwhere the application is run.\n\nIn general these directories need to be manually created\nby the application at runtime, before accessing files or folders\nin it is possible.\n\nTherefore, it is also allowed to create all of these folders via\nthe `mkdir` command.\n\n#### Denied Permissions\n\nThis default permission set prevents access to critical components\nof the Tauri application by default.\nOn Windows the webview data folder access is denied.\n\n#### Included permissions within this default permission set:\n",
"type": "string",
"const": "default"
},

@ -15,14 +15,14 @@ use tauri::{
use std::{
borrow::Cow,
fs::File,
io::{BufReader, Lines, Read, Write},
io::{BufRead, BufReader, Read, Write},
path::{Path, PathBuf},
str::FromStr,
sync::Mutex,
time::{SystemTime, UNIX_EPOCH},
};
use crate::{scope::Entry, Error, FsExt, SafeFilePath};
use crate::{scope::Entry, Error, SafeFilePath};
#[derive(Debug, thiserror::Error)]
pub enum CommandError {
@ -245,32 +245,12 @@ pub fn mkdir<R: Runtime>(
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct DirEntry {
pub name: Option<String>,
pub name: String,
pub is_directory: bool,
pub is_file: bool,
pub is_symlink: bool,
}
fn read_dir_inner<P: AsRef<Path>>(path: P) -> crate::Result<Vec<DirEntry>> {
let mut files_and_dirs: Vec<DirEntry> = vec![];
for entry in std::fs::read_dir(path)? {
let path = entry?.path();
let file_type = path.metadata()?.file_type();
files_and_dirs.push(DirEntry {
is_directory: file_type.is_dir(),
is_file: file_type.is_file(),
is_symlink: std::fs::symlink_metadata(&path)
.map(|md| md.file_type().is_symlink())
.unwrap_or(false),
name: path
.file_name()
.map(|name| name.to_string_lossy())
.map(|name| name.to_string()),
});
}
Result::Ok(files_and_dirs)
}
#[tauri::command]
pub async fn read_dir<R: Runtime>(
webview: Webview<R>,
@ -287,14 +267,37 @@ pub async fn read_dir<R: Runtime>(
options.as_ref().and_then(|o| o.base_dir),
)?;
read_dir_inner(&resolved_path)
.map_err(|e| {
let entries = std::fs::read_dir(&resolved_path).map_err(|e| {
format!(
"failed to read directory at path: {} with error: {e}",
resolved_path.display()
)
})?;
let entries = entries
.filter_map(|entry| {
let entry = entry.ok()?;
let name = entry.file_name().into_string().ok()?;
let metadata = entry.file_type();
macro_rules! method_or_false {
($method:ident) => {
if let Ok(metadata) = &metadata {
metadata.$method()
} else {
false
}
};
}
Some(DirEntry {
name,
is_file: method_or_false!(is_file),
is_directory: method_or_false!(is_dir),
is_symlink: method_or_false!(is_symlink),
})
.map_err(Into::into)
})
.collect();
Ok(entries)
}
#[tauri::command]
@ -369,6 +372,7 @@ pub async fn read_file<R: Runtime>(
Ok(tauri::ipc::Response::new(contents))
}
// TODO, remove in v3, rely on `read_file` command instead
#[tauri::command]
pub async fn read_text_file<R: Runtime>(
webview: Webview<R>,
@ -376,33 +380,8 @@ pub async fn read_text_file<R: Runtime>(
command_scope: CommandScope<Entry>,
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<String> {
let (mut file, path) = resolve_file(
&webview,
&global_scope,
&command_scope,
path,
OpenOptions {
base: BaseOptions {
base_dir: options.as_ref().and_then(|o| o.base_dir),
},
options: crate::OpenOptions {
read: true,
..Default::default()
},
},
)?;
let mut contents = String::new();
file.read_to_string(&mut contents).map_err(|e| {
format!(
"failed to read file as text at path: {} with error: {e}",
path.display()
)
})?;
Ok(contents)
) -> CommandResult<tauri::ipc::Response> {
read_file(webview, global_scope, command_scope, path, options).await
}
#[tauri::command]
@ -413,8 +392,6 @@ pub fn read_text_file_lines<R: Runtime>(
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<ResourceId> {
use std::io::BufRead;
let resolved_path = resolve_path(
&webview,
&global_scope,
@ -430,7 +407,7 @@ pub fn read_text_file_lines<R: Runtime>(
)
})?;
let lines = BufReader::new(file).lines();
let lines = BufReader::new(file);
let rid = webview.resources_table().add(StdLinesResource::new(lines));
Ok(rid)
@ -440,18 +417,28 @@ pub fn read_text_file_lines<R: Runtime>(
pub async fn read_text_file_lines_next<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
) -> CommandResult<(Option<String>, bool)> {
) -> CommandResult<tauri::ipc::Response> {
let mut resource_table = webview.resources_table();
let lines = resource_table.get::<StdLinesResource>(rid)?;
let ret = StdLinesResource::with_lock(&lines, |lines| {
lines.next().map(|a| (a.ok(), false)).unwrap_or_else(|| {
let _ = resource_table.close(rid);
(None, true)
})
let ret = StdLinesResource::with_lock(&lines, |lines| -> CommandResult<Vec<u8>> {
// This is an optimization to include wether we finished iteration or not (1 or 0)
// at the end of returned vector so we can use `tauri::ipc::Response`
// and avoid serialization overhead of separate values.
match lines.next() {
Some(Ok(mut bytes)) => {
bytes.push(false as u8);
Ok(bytes)
}
Some(Err(_)) => Ok(vec![false as u8]),
None => {
resource_table.close(rid)?;
Ok(vec![true as u8])
}
}
});
Ok(ret)
ret.map(tauri::ipc::Response::new)
}
#[derive(Debug, Clone, Deserialize)]
@ -802,10 +789,11 @@ fn default_create_value() -> bool {
true
}
fn write_file_inner<R: Runtime>(
#[tauri::command]
pub async fn write_file<R: Runtime>(
webview: Webview<R>,
global_scope: &GlobalScope<Entry>,
command_scope: &CommandScope<Entry>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
request: tauri::ipc::Request<'_>,
) -> CommandResult<()> {
let data = match request.body() {
@ -836,8 +824,8 @@ fn write_file_inner<R: Runtime>(
let (mut file, path) = resolve_file(
&webview,
global_scope,
command_scope,
&global_scope,
&command_scope,
path,
if let Some(opts) = options {
OpenOptions {
@ -880,35 +868,43 @@ fn write_file_inner<R: Runtime>(
.map_err(Into::into)
}
// TODO, remove in v3, rely on `write_file` command instead
#[tauri::command]
pub async fn write_file<R: Runtime>(
pub async fn write_text_file<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
request: tauri::ipc::Request<'_>,
) -> CommandResult<()> {
write_file_inner(webview, &global_scope, &command_scope, request)
write_file(webview, global_scope, command_scope, request).await
}
// TODO, in v3, remove this command and rely on `write_file` command only
#[tauri::command]
pub async fn write_text_file<R: Runtime>(
pub fn exists<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
request: tauri::ipc::Request<'_>,
) -> CommandResult<()> {
write_file_inner(webview, &global_scope, &command_scope, request)
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<bool> {
let resolved_path = resolve_path(
&webview,
&global_scope,
&command_scope,
path,
options.as_ref().and_then(|o| o.base_dir),
)?;
Ok(resolved_path.exists())
}
#[tauri::command]
pub fn exists<R: Runtime>(
pub async fn size<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<bool> {
) -> CommandResult<u64> {
let resolved_path = resolve_path(
&webview,
&global_scope,
@ -916,7 +912,38 @@ pub fn exists<R: Runtime>(
path,
options.as_ref().and_then(|o| o.base_dir),
)?;
Ok(resolved_path.exists())
let metadata = resolved_path.metadata()?;
if metadata.is_file() {
Ok(metadata.len())
} else {
let size = get_dir_size(&resolved_path).map_err(|e| {
format!(
"failed to get size at path: {} with error: {e}",
resolved_path.display()
)
})?;
Ok(size)
}
}
fn get_dir_size(path: &PathBuf) -> CommandResult<u64> {
let mut size = 0;
for entry in std::fs::read_dir(path)? {
let entry = entry?;
let metadata = entry.metadata()?;
if metadata.is_file() {
size += metadata.len();
} else if metadata.is_dir() {
size += get_dir_size(&entry.path())?;
}
}
Ok(size)
}
#[cfg(not(target_os = "android"))]
@ -964,6 +991,8 @@ pub fn resolve_file<R: Runtime>(
path: SafeFilePath,
open_options: OpenOptions,
) -> CommandResult<(File, PathBuf)> {
use crate::FsExt;
match path {
SafeFilePath::Url(url) => {
let path = url.as_str().into();
@ -996,40 +1025,81 @@ pub fn resolve_path<R: Runtime>(
path
};
let fs_scope = webview.state::<crate::Scope>();
let scope = tauri::scope::fs::Scope::new(
webview,
&FsScope::Scope {
allow: webview
.fs_scope()
.allowed
.lock()
.unwrap()
.clone()
.into_iter()
.chain(global_scope.allows().iter().filter_map(|e| e.path.clone()))
allow: global_scope
.allows()
.iter()
.filter_map(|e| e.path.clone())
.chain(command_scope.allows().iter().filter_map(|e| e.path.clone()))
.collect(),
deny: webview
.fs_scope()
.denied
.lock()
.unwrap()
.clone()
.into_iter()
.chain(global_scope.denies().iter().filter_map(|e| e.path.clone()))
deny: global_scope
.denies()
.iter()
.filter_map(|e| e.path.clone())
.chain(command_scope.denies().iter().filter_map(|e| e.path.clone()))
.collect(),
require_literal_leading_dot: webview.fs_scope().require_literal_leading_dot,
require_literal_leading_dot: fs_scope.require_literal_leading_dot,
},
)?;
if scope.is_allowed(&path) {
let require_literal_leading_dot = fs_scope.require_literal_leading_dot.unwrap_or(cfg!(unix));
if is_forbidden(&fs_scope.scope, &path, require_literal_leading_dot)
|| is_forbidden(&scope, &path, require_literal_leading_dot)
{
return Err(CommandError::Plugin(Error::PathForbidden(path)));
}
if fs_scope.scope.is_allowed(&path) || scope.is_allowed(&path) {
Ok(path)
} else {
Err(CommandError::Plugin(Error::PathForbidden(path)))
}
}
fn is_forbidden<P: AsRef<Path>>(
scope: &tauri::fs::Scope,
path: P,
require_literal_leading_dot: bool,
) -> bool {
let path = path.as_ref();
let path = if path.is_symlink() {
match std::fs::read_link(path) {
Ok(p) => p,
Err(_) => return false,
}
} else {
path.to_path_buf()
};
let path = if !path.exists() {
crate::Result::Ok(path)
} else {
std::fs::canonicalize(path).map_err(Into::into)
};
if let Ok(path) = path {
let path: PathBuf = path.components().collect();
scope.forbidden_patterns().iter().any(|p| {
p.matches_path_with(
&path,
glob::MatchOptions {
// this is needed so `/dir/*` doesn't match files within subdirectories such as `/dir/subdir/file.txt`
// see: <https://github.com/tauri-apps/tauri/security/advisories/GHSA-6mv3-wm7j-h4w5>
require_literal_separator: true,
require_literal_leading_dot,
..Default::default()
},
)
})
} else {
false
}
}
struct StdFileResource(Mutex<File>);
impl StdFileResource {
@ -1045,14 +1115,38 @@ impl StdFileResource {
impl Resource for StdFileResource {}
struct StdLinesResource(Mutex<Lines<BufReader<File>>>);
/// Same as [std::io::Lines] but with bytes
struct LinesBytes<T: BufRead>(T);
impl<B: BufRead> Iterator for LinesBytes<B> {
type Item = std::io::Result<Vec<u8>>;
fn next(&mut self) -> Option<std::io::Result<Vec<u8>>> {
let mut buf = Vec::new();
match self.0.read_until(b'\n', &mut buf) {
Ok(0) => None,
Ok(_n) => {
if buf.last() == Some(&b'\n') {
buf.pop();
if buf.last() == Some(&b'\r') {
buf.pop();
}
}
Some(Ok(buf))
}
Err(e) => Some(Err(e)),
}
}
}
struct StdLinesResource(Mutex<LinesBytes<BufReader<File>>>);
impl StdLinesResource {
fn new(lines: Lines<BufReader<File>>) -> Self {
Self(Mutex::new(lines))
fn new(lines: BufReader<File>) -> Self {
Self(Mutex::new(LinesBytes(lines)))
}
fn with_lock<R, F: FnMut(&mut Lines<BufReader<File>>) -> R>(&self, mut f: F) -> R {
fn with_lock<R, F: FnMut(&mut LinesBytes<BufReader<File>>) -> R>(&self, mut f: F) -> R {
let mut lines = self.0.lock().unwrap();
f(&mut lines)
}
@ -1151,7 +1245,12 @@ fn get_stat(metadata: std::fs::Metadata) -> FileInfo {
}
}
#[cfg(test)]
mod test {
use std::io::{BufRead, BufReader};
use super::LinesBytes;
#[test]
fn safe_file_path_parse() {
use super::SafeFilePath;
@ -1165,4 +1264,24 @@ mod test {
Ok(SafeFilePath::Url(_))
));
}
#[test]
fn test_lines_bytes() {
let base = String::from("line 1\nline2\nline 3\nline 4");
let bytes = base.as_bytes();
let string1 = base.lines().collect::<String>();
let string2 = BufReader::new(bytes)
.lines()
.map_while(Result::ok)
.collect::<String>();
let string3 = LinesBytes(BufReader::new(bytes))
.flatten()
.flat_map(String::from_utf8)
.collect::<String>();
assert_eq!(string1, string2);
assert_eq!(string1, string3);
assert_eq!(string2, string3);
}
}

@ -138,7 +138,7 @@ impl<'de> serde::Deserialize<'de> for FilePath {
{
struct FilePathVisitor;
impl<'de> serde::de::Visitor<'de> for FilePathVisitor {
impl serde::de::Visitor<'_> for FilePathVisitor {
type Value = FilePath;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
@ -169,7 +169,7 @@ impl<'de> serde::Deserialize<'de> for SafeFilePath {
{
struct SafeFilePathVisitor;
impl<'de> serde::de::Visitor<'de> for SafeFilePathVisitor {
impl serde::de::Visitor<'_> for SafeFilePathVisitor {
type Value = SafeFilePath;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {

@ -2,8 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
// 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(
@ -17,7 +15,7 @@ use serde::Deserialize;
use tauri::{
ipc::ScopeObject,
plugin::{Builder as PluginBuilder, TauriPlugin},
utils::acl::Value,
utils::{acl::Value, config::FsScope},
AppHandle, DragDropEvent, Manager, RunEvent, Runtime, WindowEvent,
};
@ -41,7 +39,6 @@ pub use desktop::Fs;
pub use mobile::Fs;
pub use error::Error;
pub use scope::{Event as ScopeEvent, Scope};
pub use file_path::FilePath;
pub use file_path::SafeFilePath;
@ -367,21 +364,26 @@ impl ScopeObject for scope::Entry {
}
}
pub(crate) struct Scope {
pub(crate) scope: tauri::fs::Scope,
pub(crate) require_literal_leading_dot: Option<bool>,
}
pub trait FsExt<R: Runtime> {
fn fs_scope(&self) -> &Scope;
fn try_fs_scope(&self) -> Option<&Scope>;
fn fs_scope(&self) -> tauri::fs::Scope;
fn try_fs_scope(&self) -> Option<tauri::fs::Scope>;
/// Cross platform file system APIs that also support manipulating Android files.
fn fs(&self) -> &Fs<R>;
}
impl<R: Runtime, T: Manager<R>> FsExt<R> for T {
fn fs_scope(&self) -> &Scope {
self.state::<Scope>().inner()
fn fs_scope(&self) -> tauri::fs::Scope {
self.state::<Scope>().scope.clone()
}
fn try_fs_scope(&self) -> Option<&Scope> {
self.try_state::<Scope>().map(|s| s.inner())
fn try_fs_scope(&self) -> Option<tauri::fs::Scope> {
self.try_state::<Scope>().map(|s| s.scope.clone())
}
fn fs(&self) -> &Fs<R> {
@ -415,17 +417,20 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
commands::write_file,
commands::write_text_file,
commands::exists,
commands::size,
#[cfg(feature = "watch")]
watcher::watch,
#[cfg(feature = "watch")]
watcher::unwatch
])
.setup(|app, api| {
let mut scope = Scope::default();
scope.require_literal_leading_dot = api
let scope = Scope {
require_literal_leading_dot: api
.config()
.as_ref()
.and_then(|c| c.require_literal_leading_dot);
.and_then(|c| c.require_literal_leading_dot),
scope: tauri::fs::Scope::new(app, &FsScope::default())?,
};
#[cfg(target_os = "android")]
{
@ -448,9 +453,9 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
let scope = app.fs_scope();
for path in paths {
if path.is_file() {
scope.allow_file(path);
let _ = scope.allow_file(path);
} else {
scope.allow_directory(path, true);
let _ = scope.allow_directory(path, true);
}
}
}

@ -2,130 +2,18 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::{
atomic::{AtomicU32, Ordering},
Mutex,
},
};
use std::path::PathBuf;
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(untagged)]
pub(crate) enum EntryRaw {
Value(PathBuf),
Object { path: PathBuf },
}
#[derive(Debug)]
pub struct Entry {
pub path: Option<PathBuf>,
}
pub type EventId = u32;
type EventListener = Box<dyn Fn(&Event) + Send>;
/// Scope change event.
#[derive(Debug, Clone)]
pub enum Event {
/// A path has been allowed.
PathAllowed(PathBuf),
/// A path has been forbidden.
PathForbidden(PathBuf),
}
#[derive(Default)]
pub struct Scope {
pub(crate) allowed: Mutex<Vec<PathBuf>>,
pub(crate) denied: Mutex<Vec<PathBuf>>,
event_listeners: Mutex<HashMap<EventId, EventListener>>,
next_event_id: AtomicU32,
pub(crate) require_literal_leading_dot: Option<bool>,
}
impl Scope {
/// Extend the allowed patterns with the given directory.
///
/// After this function has been called, the frontend will be able to use the Tauri API to read
/// the directory and all of its files. If `recursive` is `true`, subdirectories will be accessible too.
pub fn allow_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) {
let path = path.as_ref();
{
let mut allowed = self.allowed.lock().unwrap();
allowed.push(path.to_path_buf());
allowed.push(path.join(if recursive { "**" } else { "*" }));
}
self.emit(Event::PathAllowed(path.to_path_buf()));
}
/// Extend the allowed patterns with the given file path.
///
/// After this function has been called, the frontend will be able to use the Tauri API to read the contents of this file.
pub fn allow_file<P: AsRef<Path>>(&self, path: P) {
let path = path.as_ref();
self.allowed.lock().unwrap().push(path.to_path_buf());
self.emit(Event::PathAllowed(path.to_path_buf()));
}
/// Set the given directory path to be forbidden by this scope.
///
/// **Note:** this takes precedence over allowed paths, so its access gets denied **always**.
pub fn forbid_directory<P: AsRef<Path>>(&self, path: P, recursive: bool) {
let path = path.as_ref();
{
let mut denied = self.denied.lock().unwrap();
denied.push(path.to_path_buf());
denied.push(path.join(if recursive { "**" } else { "*" }));
}
self.emit(Event::PathForbidden(path.to_path_buf()));
}
/// Set the given file path to be forbidden by this scope.
///
/// **Note:** this takes precedence over allowed paths, so its access gets denied **always**.
pub fn forbid_file<P: AsRef<Path>>(&self, path: P) {
let path = path.as_ref();
self.denied.lock().unwrap().push(path.to_path_buf());
self.emit(Event::PathForbidden(path.to_path_buf()));
}
/// List of allowed paths.
pub fn allowed(&self) -> Vec<PathBuf> {
self.allowed.lock().unwrap().clone()
}
/// List of forbidden paths.
pub fn forbidden(&self) -> Vec<PathBuf> {
self.denied.lock().unwrap().clone()
}
fn next_event_id(&self) -> u32 {
self.next_event_id.fetch_add(1, Ordering::Relaxed)
}
fn emit(&self, event: Event) {
let listeners = self.event_listeners.lock().unwrap();
let handlers = listeners.values();
for listener in handlers {
listener(&event);
}
}
/// Listen to an event on this scope.
pub fn listen<F: Fn(&Event) + Send + 'static>(&self, f: F) -> EventId {
let id = self.next_event_id();
self.event_listeners.lock().unwrap().insert(id, Box::new(f));
id
}
#[derive(Deserialize)]
#[serde(untagged)]
pub(crate) enum EntryRaw {
Value(PathBuf),
Object { path: PathBuf },
}

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
use notify_debouncer_full::{new_debouncer, DebounceEventResult, Debouncer, FileIdMap};
use notify_debouncer_full::{new_debouncer, DebounceEventResult, Debouncer, RecommendedCache};
use serde::Deserialize;
use tauri::{
ipc::{Channel, CommandScope, GlobalScope},
@ -47,7 +47,7 @@ impl WatcherResource {
impl Resource for WatcherResource {}
enum WatcherKind {
Debouncer(Debouncer<RecommendedWatcher, FileIdMap>),
Debouncer(Debouncer<RecommendedWatcher, RecommendedCache>),
Watcher(RecommendedWatcher),
}
@ -111,8 +111,7 @@ pub async fn watch<R: Runtime>(
let (tx, rx) = channel();
let mut debouncer = new_debouncer(Duration::from_millis(delay), None, tx)?;
for path in &resolved_paths {
debouncer.watcher().watch(path.as_ref(), recursive_mode)?;
debouncer.cache().add_root(path, recursive_mode);
debouncer.watch(path, recursive_mode)?;
}
watch_debounced(on_event, rx);
WatcherKind::Debouncer(debouncer)
@ -120,7 +119,7 @@ pub async fn watch<R: Runtime>(
let (tx, rx) = channel();
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
for path in &resolved_paths {
watcher.watch(path.as_ref(), recursive_mode)?;
watcher.watch(path, recursive_mode)?;
}
watch_raw(on_event, rx);
WatcherKind::Watcher(watcher)
@ -140,14 +139,14 @@ pub async fn unwatch<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> Comman
match &mut watcher.kind {
WatcherKind::Debouncer(ref mut debouncer) => {
for path in &watcher.paths {
debouncer.watcher().unwatch(path.as_ref()).map_err(|e| {
debouncer.unwatch(path).map_err(|e| {
format!("failed to unwatch path: {} with error: {e}", path.display())
})?;
}
}
WatcherKind::Watcher(ref mut w) => {
for path in &watcher.paths {
w.unwatch(path.as_ref()).map_err(|e| {
w.unwatch(path).map_err(|e| {
format!("failed to unwatch path: {} with error: {e}", path.display())
})?;
}

@ -81,7 +81,7 @@ The Google Play Store uses this property to decide whether it should show the ap
First you need to register the core plugin with Tauri:
`src-tauri/src/main.rs`
`src-tauri/src/lib.rs`
```rust
fn main() {
@ -92,6 +92,20 @@ fn main() {
}
```
Then, for instance, grant the plugin the permission to check or request permissions from the user and to read the device position
`src-tauri/capabilities/default.json`
```json
"permissions": [
"core:default",
"geolocation:allow-check-permissions",
"geolocation:allow-request-permissions",
"geolocation:allow-get-current-position",
"geolocation:allow-watch-position",
]
```
Afterwards all the plugin's APIs are available through the JavaScript guest bindings:
```javascript
@ -100,7 +114,7 @@ import {
requestPermissions,
getCurrentPosition,
watchPosition
} from '@tauri-apps/plugin-log'
} from '@tauri-apps/plugin-geolocation'
let permissions = await checkPermissions()
if (

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_GEOLOCATION__=function(t){"use strict";function e(t,e,n,i){if("a"===n&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof e?t!==e||!i:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===n?i:"a"===n?i.call(t):i?i.value:e.get(t)}function n(t,e,n,i,o){if("function"==typeof e?t!==e||!o:!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(t,n),n}var i,o,s;"function"==typeof SuppressedError&&SuppressedError;class r{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,i.set(this,(()=>{})),o.set(this,0),s.set(this,{}),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}((({message:t,id:r})=>{if(r===e(this,o,"f")){n(this,o,r+1),e(this,i,"f").call(this,t);const a=Object.keys(e(this,s,"f"));if(a.length>0){let t=r+1;for(const n of a.sort()){if(parseInt(n)!==t)break;{const o=e(this,s,"f")[n];delete e(this,s,"f")[n],e(this,i,"f").call(this,o),t+=1}}n(this,o,t)}}else e(this,s,"f")[r.toString()]=t}))}set onmessage(t){n(this,i,t)}get onmessage(){return e(this,i,"f")}toJSON(){return`__CHANNEL__:${this.id}`}}async function a(t,e={},n){return window.__TAURI_INTERNALS__.invoke(t,e,n)}return i=new WeakMap,o=new WeakMap,s=new WeakMap,t.checkPermissions=async function(){return await async function(t){return a(`plugin:${t}|check_permissions`)}("geolocation")},t.clearWatch=async function(t){await a("plugin:geolocation|clear_watch",{channelId:t})},t.getCurrentPosition=async function(t){return await a("plugin:geolocation|get_current_position",{options:t})},t.requestPermissions=async function(t){return await a("plugin:geolocation|request_permissions",{permissions:t})},t.watchPosition=async function(t,e){const n=new r;return n.onmessage=t=>{"string"==typeof t?e(null,t):e(t)},await a("plugin:geolocation|watch_position",{options:t,channel:n}),n.id},t}({});Object.defineProperty(window.__TAURI__,"geolocation",{value:__TAURI_PLUGIN_GEOLOCATION__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_GEOLOCATION__=function(t){"use strict";function e(t,e,n,i){if("a"===n&&!i)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof e?t!==e||!i:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===n?i:"a"===n?i.call(t):i?i.value:e.get(t)}function n(t,e,n,i,o){if("function"==typeof e?t!==e||!o:!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(t,n),n}var i,o,s;"function"==typeof SuppressedError&&SuppressedError;const r="__TAURI_TO_IPC_KEY__";class a{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,i.set(this,(()=>{})),o.set(this,0),s.set(this,{}),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}((({message:t,id:r})=>{if(r===e(this,o,"f")){n(this,o,r+1),e(this,i,"f").call(this,t);const a=Object.keys(e(this,s,"f"));if(a.length>0){let t=r+1;for(const n of a.sort()){if(parseInt(n)!==t)break;{const o=e(this,s,"f")[n];delete e(this,s,"f")[n],e(this,i,"f").call(this,o),t+=1}}n(this,o,t)}}else e(this,s,"f")[r.toString()]=t}))}set onmessage(t){n(this,i,t)}get onmessage(){return e(this,i,"f")}[(i=new WeakMap,o=new WeakMap,s=new WeakMap,r)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[r]()}}async function c(t,e={},n){return window.__TAURI_INTERNALS__.invoke(t,e,n)}return t.checkPermissions=async function(){return await async function(t){return c(`plugin:${t}|check_permissions`)}("geolocation")},t.clearWatch=async function(t){await c("plugin:geolocation|clear_watch",{channelId:t})},t.getCurrentPosition=async function(t){return await c("plugin:geolocation|get_current_position",{options:t})},t.requestPermissions=async function(t){return await c("plugin:geolocation|request_permissions",{permissions:t})},t.watchPosition=async function(t,e){const n=new a;return n.onmessage=t=>{"string"==typeof t?e(null,t):e(t)},await c("plugin:geolocation|watch_position",{options:t,channel:n}),n.id},t}({});Object.defineProperty(window.__TAURI__,"geolocation",{value:__TAURI_PLUGIN_GEOLOCATION__})}

@ -55,7 +55,7 @@ yarn add https://github.com/tauri-apps/tauri-plugin-global-shortcut#v2
First you need to register the core plugin with Tauri:
`src-tauri/src/main.rs`
`src-tauri/src/lib.rs`
```rust
fn main() {

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_GLOBAL_SHORTCUT__=function(t){"use strict";function e(t,e,r,s){if("a"===r&&!s)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof e?t!==e||!s:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?s:"a"===r?s.call(t):s?s.value:e.get(t)}function r(t,e,r,s,n){if("function"==typeof e?t!==e||!n:!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(t,r),r}var s,n,i;"function"==typeof SuppressedError&&SuppressedError;class o{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,s.set(this,(()=>{})),n.set(this,0),i.set(this,{}),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}((({message:t,id:o})=>{if(o===e(this,n,"f")){r(this,n,o+1),e(this,s,"f").call(this,t);const a=Object.keys(e(this,i,"f"));if(a.length>0){let t=o+1;for(const r of a.sort()){if(parseInt(r)!==t)break;{const n=e(this,i,"f")[r];delete e(this,i,"f")[r],e(this,s,"f").call(this,n),t+=1}}r(this,n,t)}}else e(this,i,"f")[o.toString()]=t}))}set onmessage(t){r(this,s,t)}get onmessage(){return e(this,s,"f")}toJSON(){return`__CHANNEL__:${this.id}`}}async function a(t,e={},r){return window.__TAURI_INTERNALS__.invoke(t,e,r)}return s=new WeakMap,n=new WeakMap,i=new WeakMap,t.isRegistered=async function(t){return await a("plugin:global-shortcut|is_registered",{shortcut:t})},t.register=async function(t,e){const r=new o;return r.onmessage=e,await a("plugin:global-shortcut|register",{shortcuts:Array.isArray(t)?t:[t],handler:r})},t.unregister=async function(t){return await a("plugin:global-shortcut|unregister",{shortcuts:Array.isArray(t)?t:[t]})},t.unregisterAll=async function(){return await a("plugin:global-shortcut|unregister_all",{})},t}({});Object.defineProperty(window.__TAURI__,"globalShortcut",{value:__TAURI_PLUGIN_GLOBAL_SHORTCUT__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_GLOBAL_SHORTCUT__=function(t){"use strict";function e(t,e,r,s){if("a"===r&&!s)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof e?t!==e||!s:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===r?s:"a"===r?s.call(t):s?s.value:e.get(t)}function r(t,e,r,s,n){if("function"==typeof e?t!==e||!n:!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(t,r),r}var s,n,i;"function"==typeof SuppressedError&&SuppressedError;const o="__TAURI_TO_IPC_KEY__";class a{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,s.set(this,(()=>{})),n.set(this,0),i.set(this,{}),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}((({message:t,id:o})=>{if(o===e(this,n,"f")){r(this,n,o+1),e(this,s,"f").call(this,t);const a=Object.keys(e(this,i,"f"));if(a.length>0){let t=o+1;for(const r of a.sort()){if(parseInt(r)!==t)break;{const n=e(this,i,"f")[r];delete e(this,i,"f")[r],e(this,s,"f").call(this,n),t+=1}}r(this,n,t)}}else e(this,i,"f")[o.toString()]=t}))}set onmessage(t){r(this,s,t)}get onmessage(){return e(this,s,"f")}[(s=new WeakMap,n=new WeakMap,i=new WeakMap,o)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[o]()}}async function _(t,e={},r){return window.__TAURI_INTERNALS__.invoke(t,e,r)}return t.isRegistered=async function(t){return await _("plugin:global-shortcut|is_registered",{shortcut:t})},t.register=async function(t,e){const r=new a;return r.onmessage=e,await _("plugin:global-shortcut|register",{shortcuts:Array.isArray(t)?t:[t],handler:r})},t.unregister=async function(t){return await _("plugin:global-shortcut|unregister",{shortcuts:Array.isArray(t)?t:[t]})},t.unregisterAll=async function(){return await _("plugin:global-shortcut|unregister_all",{})},t}({});Object.defineProperty(window.__TAURI__,"globalShortcut",{value:__TAURI_PLUGIN_GLOBAL_SHORTCUT__})}

@ -2,8 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
// 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.

@ -58,7 +58,7 @@ yarn add https://github.com/tauri-apps/tauri-plugin-haptics#v2
First you need to register the core plugin with Tauri:
`src-tauri/src/main.rs`
`src-tauri/src/lib.rs`
```rust
fn main() {

@ -1,5 +1,13 @@
# Changelog
## \[2.0.4]
- [`a3b553dd`](https://github.com/tauri-apps/plugins-workspace/commit/a3b553ddb403771aa699362c4e69a064b7731da5) ([#2079](https://github.com/tauri-apps/plugins-workspace/pull/2079) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Add tracing logs for requestes and responses behind `tracing` feature flag.
### Dependencies
- Upgraded to `fs@2.1.0`
## \[2.0.3]
### Dependencies

@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-http"
version = "2.0.3"
version = "2.0.4"
description = "Access an HTTP client written in Rust."
edition = { workspace = true }
authors = { workspace = true }
@ -34,7 +34,7 @@ serde_json = { workspace = true }
tauri = { workspace = true }
thiserror = { workspace = true }
tokio = { version = "1", features = ["sync", "macros"] }
tauri-plugin-fs = { path = "../fs", version = "2.0.3" }
tauri-plugin-fs = { path = "../fs", version = "2.1.0" }
urlpattern = "0.3"
regex = "1"
http = "1"
@ -42,6 +42,7 @@ reqwest = { version = "0.12", default-features = false }
url = { workspace = true }
data-url = "0.3"
reqwest_cookie_store = { version = "0.8", optional = true }
tracing = { workspace = true, optional = true }
[features]
default = [
@ -72,3 +73,4 @@ http2 = ["reqwest/http2"]
charset = ["reqwest/charset"]
macos-system-configuration = ["reqwest/macos-system-configuration"]
unsafe-headers = []
tracing = ["dep:tracing"]

@ -54,7 +54,7 @@ yarn add https://github.com/tauri-apps/tauri-plugin-http#v2
First you need to register the core plugin with Tauri:
`src-tauri/src/main.rs`
`src-tauri/src/lib.rs`
```rust
fn main() {

@ -191,6 +191,11 @@ pub async fn fetch<R: Runtime>(
let name = HeaderName::from_str(&h)?;
#[cfg(not(feature = "unsafe-headers"))]
if is_unsafe_header(&name) {
#[cfg(debug_assertions)]
{
eprintln!("[\x1b[33mWARNING\x1b[0m] Skipping {name} header as it is a forbidden header per fetch spec https://fetch.spec.whatwg.org/#terminology-headers");
eprintln!("[\x1b[33mWARNING\x1b[0m] if keeping the header is a desired behavior, you can enable `unsafe-headers` feature flag in your Cargo.toml");
}
continue;
}
@ -278,6 +283,9 @@ pub async fn fetch<R: Runtime>(
request = request.headers(headers);
#[cfg(feature = "tracing")]
tracing::trace!("{:?}", request);
let fut = async move { request.send().await.map_err(Into::into) };
let mut resources_table = webview.resources_table();
let rid = resources_table.add_request(Box::pin(fut));
@ -299,6 +307,9 @@ pub async fn fetch<R: Runtime>(
.header(header::CONTENT_TYPE, data_url.mime_type().to_string())
.body(reqwest::Body::from(body))?;
#[cfg(feature = "tracing")]
tracing::trace!("{:?}", response);
let fut = async move { Ok(reqwest::Response::from(response)) };
let mut resources_table = webview.resources_table();
let rid = resources_table.add_request(Box::pin(fut));
@ -346,6 +357,9 @@ pub async fn fetch_send<R: Runtime>(
}
};
#[cfg(feature = "tracing")]
tracing::trace!("{:?}", res);
let status = res.status();
let url = res.url().to_string();
let mut headers = Vec::new();

@ -2,8 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
//! [![](https://github.com/tauri-apps/plugins-workspace/raw/v2/plugins/http/banner.png)](https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/http)
//!
//! Access the HTTP client written in Rust.
pub use reqwest;

@ -1,5 +1,9 @@
# Changelog
## \[2.1.0]
- [`3449dd5a`](https://github.com/tauri-apps/plugins-workspace/commit/3449dd5a8f6d12fee8d6389c034fe47e19d72bcd) ([#1982](https://github.com/tauri-apps/plugins-workspace/pull/1982) by [@arihav](https://github.com/tauri-apps/plugins-workspace/../../arihav)) Add custom host binding to allow external access
## \[2.0.1]
- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.

@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-localhost"
version = "2.0.1"
version = "2.1.0"
description = "Expose your apps assets through a localhost server instead of the default custom protocol."
authors = { workspace = true }
license = { workspace = true }

@ -38,7 +38,7 @@ tauri-plugin-localhost = { git = "https://github.com/tauri-apps/plugins-workspac
First you need to register the core plugin with Tauri:
`src-tauri/src/main.rs`
`src-tauri/src/lib.rs`
```rust
use tauri::{Manager, window::WindowBuilder, WindowUrl};

@ -2,8 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
// 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.**
@ -46,6 +44,7 @@ type OnRequest = Option<Box<dyn Fn(&Request, &mut Response) + Send + Sync>>;
pub struct Builder {
port: u16,
host: Option<String>,
on_request: OnRequest,
}
@ -53,10 +52,17 @@ impl Builder {
pub fn new(port: u16) -> Self {
Self {
port,
host: None,
on_request: None,
}
}
// Change the host the plugin binds to. Defaults to `localhost`.
pub fn host<H: Into<String>>(mut self, host: H) -> Self {
self.host = Some(host.into());
self
}
pub fn on_request<F: Fn(&Request, &mut Response) + Send + Sync + 'static>(
mut self,
f: F,
@ -67,6 +73,7 @@ impl Builder {
pub fn build<R: Runtime>(mut self) -> TauriPlugin<R> {
let port = self.port;
let host = self.host.unwrap_or("localhost".to_string());
let on_request = self.on_request.take();
PluginBuilder::new("localhost")
@ -74,7 +81,7 @@ impl Builder {
let asset_resolver = app.asset_resolver();
std::thread::spawn(move || {
let server =
Server::http(format!("localhost:{port}")).expect("Unable to spawn server");
Server::http(format!("{host}:{port}")).expect("Unable to spawn server");
for req in server.incoming_requests() {
let path = req
.url()

@ -2,6 +2,14 @@
## \[2.0.1]
- [`371a2f73`](https://github.com/tauri-apps/plugins-workspace/commit/371a2f7361e0b91cf66f1287ffb18b34414a6cb8) ([#2021](https://github.com/tauri-apps/plugins-workspace/pull/2021) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Make webview log target more consistent that it always starts with `webview`
## \[2.0.2]
- [`606fa08d`](https://github.com/tauri-apps/plugins-workspace/commit/606fa08dae1acd074b961fb360623f4c86f13ee8) ([#1997](https://github.com/tauri-apps/plugins-workspace/pull/1997) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) **Potentially breaking:** Updated `fern` from 0.6 to 0.7. This is technically a breaking change because `fern` is re-exported in `tauri-plugin-log`.
## \[2.0.1]
- [`a1a82208`](https://github.com/tauri-apps/plugins-workspace/commit/a1a82208ed4ab87f83310be0dc95428aec9ab241) ([#1873](https://github.com/tauri-apps/plugins-workspace/pull/1873) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Downgrade MSRV to 1.77.2 to support Windows 7.
## \[2.0.0]

@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-log"
version = "2.0.1"
version = "2.0.3"
description = "Configurable logging for your Tauri app."
authors = { workspace = true }
license = { workspace = true }
@ -27,12 +27,12 @@ tauri-plugin = { workspace = true, features = ["build"] }
serde = { workspace = true }
serde_json = { workspace = true }
tauri = { workspace = true }
thiserror = { workspace = true }
serde_repr = "0.1"
byte-unit = "5"
log = { workspace = true, features = ["kv_unstable"] }
time = { version = "0.3", features = ["formatting", "local-offset"] }
fern = "0.6"
thiserror = "1"
fern = "0.7"
[target."cfg(target_os = \"android\")".dependencies]
android_logger = "0.14"

@ -56,7 +56,7 @@ yarn add https://github.com/tauri-apps/tauri-plugin-log#v2
First you need to register the core plugin with Tauri:
`src-tauri/src/main.rs`
`src-tauri/src/lib.rs`
```rust
use tauri_plugin_log::{Target, TargetKind};

@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_LOG__=function(e){"use strict";function n(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}async function r(e,n={},r){return window.__TAURI_INTERNALS__.invoke(e,n,r)}var a,t;async function o(e,a,t){const o={kind:"Any"};return r("plugin:event|listen",{event:e,target:o,handler:n(a)}).then((n=>async()=>async function(e,n){await r("plugin:event|unlisten",{event:e,eventId:n})}(e,n)))}async function i(e,n,a){const t=(new Error).stack?.split("\n").map((e=>e.split("@"))),o=t?.filter((([e,n])=>e.length>0&&"[native code]"!==n)),{file:i,line:c,keyValues:u}=a??{};let l=o?.[0]?.filter((e=>e.length>0)).join("@");"Error"===l&&(l="webview::unknown"),await r("plugin:log|log",{level:e,message:n,location:l,file:i,line:c,keyValues:u})}async function c(e){return await o("log://log",(n=>{const{level:r}=n.payload;let{message:a}=n.payload;a=a.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,""),e({message:a,level:r})}))}return"function"==typeof SuppressedError&&SuppressedError,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG_ENTER="tauri://drag-enter",e.DRAG_OVER="tauri://drag-over",e.DRAG_DROP="tauri://drag-drop",e.DRAG_LEAVE="tauri://drag-leave"}(a||(a={})),function(e){e[e.Trace=1]="Trace",e[e.Debug=2]="Debug",e[e.Info=3]="Info",e[e.Warn=4]="Warn",e[e.Error=5]="Error"}(t||(t={})),e.attachConsole=async function(){return await c((({level:e,message:n})=>{switch(e){case t.Trace:console.log(n);break;case t.Debug:console.debug(n);break;case t.Info:console.info(n);break;case t.Warn:console.warn(n);break;case t.Error:console.error(n);break;default:throw new Error(`unknown log level ${e}`)}}))},e.attachLogger=c,e.debug=async function(e,n){await i(t.Debug,e,n)},e.error=async function(e,n){await i(t.Error,e,n)},e.info=async function(e,n){await i(t.Info,e,n)},e.trace=async function(e,n){await i(t.Trace,e,n)},e.warn=async function(e,n){await i(t.Warn,e,n)},e}({});Object.defineProperty(window.__TAURI__,"log",{value:__TAURI_PLUGIN_LOG__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_LOG__=function(e){"use strict";function n(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}async function r(e,n={},r){return window.__TAURI_INTERNALS__.invoke(e,n,r)}var a,t;async function o(e,a,t){const o={kind:"Any"};return r("plugin:event|listen",{event:e,target:o,handler:n(a)}).then((n=>async()=>async function(e,n){await r("plugin:event|unlisten",{event:e,eventId:n})}(e,n)))}async function i(e,n,a){const t=function(e){if(e){if(!e.startsWith("Error")){const n=e.split("\n").map((e=>e.split("@"))).filter((([e,n])=>e.length>0&&"[native code]"!==n));return n[2]?.filter((e=>e.length>0)).join("@")}{const n=e.split("\n"),r=n[3]?.trim();if(!r)return;const a=/at\s+(?<functionName>.*?)\s+\((?<fileName>.*?):(?<lineNumber>\d+):(?<columnNumber>\d+)\)/,t=r.match(a);if(t){const{functionName:e,fileName:n,lineNumber:r,columnNumber:a}=t.groups;return`${e}@${n}:${r}:${a}`}{const e=/at\s+(?<fileName>.*?):(?<lineNumber>\d+):(?<columnNumber>\d+)/,n=r.match(e);if(n){const{fileName:e,lineNumber:r,columnNumber:a}=n.groups;return`<anonymous>@${e}:${r}:${a}`}}}}}((new Error).stack),{file:o,line:i,keyValues:u}=a??{};await r("plugin:log|log",{level:e,message:n,location:t,file:o,line:i,keyValues:u})}async function u(e){return await o("log://log",(n=>{const{level:r}=n.payload;let{message:a}=n.payload;a=a.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,""),e({message:a,level:r})}))}return"function"==typeof SuppressedError&&SuppressedError,function(e){e.WINDOW_RESIZED="tauri://resize",e.WINDOW_MOVED="tauri://move",e.WINDOW_CLOSE_REQUESTED="tauri://close-requested",e.WINDOW_DESTROYED="tauri://destroyed",e.WINDOW_FOCUS="tauri://focus",e.WINDOW_BLUR="tauri://blur",e.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",e.WINDOW_THEME_CHANGED="tauri://theme-changed",e.WINDOW_CREATED="tauri://window-created",e.WEBVIEW_CREATED="tauri://webview-created",e.DRAG_ENTER="tauri://drag-enter",e.DRAG_OVER="tauri://drag-over",e.DRAG_DROP="tauri://drag-drop",e.DRAG_LEAVE="tauri://drag-leave"}(a||(a={})),function(e){e[e.Trace=1]="Trace",e[e.Debug=2]="Debug",e[e.Info=3]="Info",e[e.Warn=4]="Warn",e[e.Error=5]="Error"}(t||(t={})),e.attachConsole=async function(){return await u((({level:e,message:n})=>{switch(e){case t.Trace:console.log(n);break;case t.Debug:console.debug(n);break;case t.Info:console.info(n);break;case t.Warn:console.warn(n);break;case t.Error:console.error(n);break;default:throw new Error(`unknown log level ${e}`)}}))},e.attachLogger=u,e.debug=async function(e,n){await i(t.Debug,e,n)},e.error=async function(e,n){await i(t.Error,e,n)},e.info=async function(e,n){await i(t.Info,e,n)},e.trace=async function(e,n){await i(t.Trace,e,n)},e.warn=async function(e,n){await i(t.Warn,e,n)},e}({});Object.defineProperty(window.__TAURI__,"log",{value:__TAURI_PLUGIN_LOG__})}

@ -44,24 +44,81 @@ enum LogLevel {
Error
}
function getCallerLocation(stack?: string) {
if (!stack) {
return
}
if (stack.startsWith('Error')) {
// Assume it's Chromium V8
//
// Error
// at baz (filename.js:10:15)
// at bar (filename.js:6:3)
// at foo (filename.js:2:3)
// at filename.js:13:1
const lines = stack.split('\n')
// Find the third line (caller's caller of the current location)
const callerLine = lines[3]?.trim()
if (!callerLine) {
return
}
const regex =
/at\s+(?<functionName>.*?)\s+\((?<fileName>.*?):(?<lineNumber>\d+):(?<columnNumber>\d+)\)/
const match = callerLine.match(regex)
if (match) {
const { functionName, fileName, lineNumber, columnNumber } =
match.groups as {
functionName: string
fileName: string
lineNumber: string
columnNumber: string
}
return `${functionName}@${fileName}:${lineNumber}:${columnNumber}`
} else {
// Handle cases where the regex does not match (e.g., last line without function name)
const regexNoFunction =
/at\s+(?<fileName>.*?):(?<lineNumber>\d+):(?<columnNumber>\d+)/
const matchNoFunction = callerLine.match(regexNoFunction)
if (matchNoFunction) {
const { fileName, lineNumber, columnNumber } =
matchNoFunction.groups as {
fileName: string
lineNumber: string
columnNumber: string
}
return `<anonymous>@${fileName}:${lineNumber}:${columnNumber}`
}
}
} else {
// Assume it's Webkit JavaScriptCore, example:
//
// baz@filename.js:10:24
// bar@filename.js:6:6
// foo@filename.js:2:6
// global code@filename.js:13:4
const traces = stack.split('\n').map((line) => line.split('@'))
const filtered = traces.filter(([name, location]) => {
return name.length > 0 && location !== '[native code]'
})
// Find the third line (caller's caller of the current location)
return filtered[2]?.filter((v) => v.length > 0).join('@')
}
}
async function log(
level: LogLevel,
message: string,
options?: LogOptions
): Promise<void> {
const traces = new Error().stack?.split('\n').map((line) => line.split('@'))
const filtered = traces?.filter(([name, location]) => {
return name.length > 0 && location !== '[native code]'
})
const location = getCallerLocation(new Error().stack)
const { file, line, keyValues } = options ?? {}
let location = filtered?.[0]?.filter((v) => v.length > 0).join('@')
if (location === 'Error') {
location = 'webview::unknown'
}
await invoke('plugin:log|log', {
level,
message,

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

Loading…
Cancel
Save