Merge branch 'v2' into v2

pull/1919/head
Night_Hunter 8 months ago committed by GitHub
commit cf710aafea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
fs: minor
---
Add the `size` method to get the size of a file or directory.

@ -0,0 +1,5 @@
---
"updater": "minor"
---
Added support for `.deb` package updates on Linux systems.

@ -0,0 +1,5 @@
---
clipboard-manager-js: patch
---
Fix clipboard manager client side api not copying fallback alternative text when calling `writeHtml`.

@ -0,0 +1,5 @@
---
dialog: patch
---
The `Dialog` struct is now correctly exported, primarily to fix the documentation on `docs.rs`.

@ -34,11 +34,11 @@
"devDependencies": {
"@iconify-json/codicon": "^1.1.37",
"@iconify-json/ph": "^1.1.8",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tauri-apps/cli": "2.1.0",
"@unocss/extractor-svelte": "^0.64.0",
"svelte": "^5.0.0",
"unocss": "^0.64.0",
"vite": "^5.4.7"
"vite": "^6.0.0"
}
}

@ -20,11 +20,11 @@
"eslint": "9.15.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-security": "3.0.1",
"prettier": "3.3.3",
"rollup": "4.27.3",
"prettier": "3.4.1",
"rollup": "4.27.4",
"tslib": "2.8.1",
"typescript": "5.6.3",
"typescript-eslint": "8.15.0"
"typescript": "5.7.2",
"typescript-eslint": "8.16.0"
},
"resolutions": {
"semver": ">=7.5.2",

@ -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__})}

@ -120,10 +120,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
})
}

@ -16,6 +16,6 @@
"devDependencies": {
"@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,7 +92,7 @@ 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
*/

@ -39,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";

File diff suppressed because one or more lines are too long

@ -102,6 +102,7 @@ const COMMANDS: &[(&str, &[&str])] = &[
("exists", &[]),
("watch", &[]),
("unwatch", &[]),
("size", &[]),
];
fn main() {

@ -1322,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,
@ -1369,5 +1394,6 @@ export {
writeTextFile,
exists,
watch,
watchImmediate
watchImmediate,
size
}

@ -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"]

@ -3410,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>

@ -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",

@ -897,6 +897,55 @@ pub fn exists<R: Runtime>(
Ok(resolved_path.exists())
}
#[tauri::command]
pub async fn size<R: Runtime>(
webview: Webview<R>,
global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>,
path: SafeFilePath,
options: Option<BaseOptions>,
) -> CommandResult<u64> {
let resolved_path = resolve_path(
&webview,
&global_scope,
&command_scope,
path,
options.as_ref().and_then(|o| o.base_dir),
)?;
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"))]
pub fn resolve_file<R: Runtime>(
webview: &Webview<R>,

@ -417,6 +417,7 @@ 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")]

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

@ -9,7 +9,7 @@
},
"devDependencies": {
"@tauri-apps/cli": "2.1.0",
"vite": "^5.0.12",
"vite": "^6.0.0",
"typescript": "^5.4.7"
}
}

@ -63,6 +63,12 @@ pub enum Error {
TempDirNotOnSameMountPoint,
#[error("binary for the current target not found in the archive")]
BinaryNotFoundInArchive,
#[error("failed to create temporary directory")]
TempDirNotFound,
#[error("Authentication failed or was cancelled")]
AuthenticationFailed,
#[error("Failed to install .deb package")]
DebInstallFailed,
#[error("invalid updater binary format")]
InvalidUpdaterFormat,
#[error(transparent)]

@ -749,7 +749,7 @@ impl Update {
}
}
/// Linux (AppImage)
/// Linux (AppImage and Deb)
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
@ -761,12 +761,19 @@ impl Update {
/// ### Expected structure:
/// ├── [AppName]_[version]_amd64.AppImage.tar.gz # GZ generated by tauri-bundler
/// │ └──[AppName]_[version]_amd64.AppImage # Application AppImage
/// ├── [AppName]_[version]_amd64.deb # Debian package
/// └── ...
///
/// We should have an AppImage already installed to be able to copy and install
/// the extract_path is the current AppImage path
/// tmp_dir is where our new AppImage is found
fn install_inner(&self, bytes: &[u8]) -> Result<()> {
if self.is_deb_package() {
self.install_deb(bytes)
} else {
// Handle AppImage or other formats
self.install_appimage(bytes)
}
}
fn install_appimage(&self, bytes: &[u8]) -> Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let extract_path_metadata = self.extract_path.metadata()?;
@ -836,6 +843,162 @@ impl Update {
Err(Error::TempDirNotOnSameMountPoint)
}
fn is_deb_package(&self) -> bool {
// First check if we're in a typical Debian installation path
let in_system_path = self
.extract_path
.to_str()
.map(|p| p.starts_with("/usr"))
.unwrap_or(false);
if !in_system_path {
return false;
}
// Then verify it's actually a Debian-based system by checking for dpkg
let dpkg_exists = std::path::Path::new("/var/lib/dpkg").exists();
let apt_exists = std::path::Path::new("/etc/apt").exists();
// Additional check for the package in dpkg database
let package_in_dpkg = if let Ok(output) = std::process::Command::new("dpkg")
.args(["-S", &self.extract_path.to_string_lossy()])
.output()
{
output.status.success()
} else {
false
};
// Consider it a deb package only if:
// 1. We're in a system path AND
// 2. We have Debian package management tools AND
// 3. The binary is tracked by dpkg
dpkg_exists && apt_exists && package_in_dpkg
}
fn install_deb(&self, bytes: &[u8]) -> Result<()> {
// First verify the bytes are actually a .deb package
if !infer::archive::is_deb(bytes) {
return Err(Error::InvalidUpdaterFormat);
}
// Try different temp directories
let tmp_dir_locations = vec![
Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>,
Box::new(dirs::cache_dir),
Box::new(|| Some(self.extract_path.parent().unwrap().to_path_buf())),
];
// Try writing to multiple temp locations until one succeeds
for tmp_dir_location in tmp_dir_locations {
if let Some(path) = tmp_dir_location() {
if let Ok(tmp_dir) = tempfile::Builder::new()
.prefix("tauri_deb_update")
.tempdir_in(path)
{
let deb_path = tmp_dir.path().join("package.deb");
// Try writing the .deb file
if std::fs::write(&deb_path, bytes).is_ok() {
// If write succeeds, proceed with installation
return self.try_install_with_privileges(&deb_path);
}
// If write fails, continue to next temp location
}
}
}
// If we get here, all temp locations failed
Err(Error::TempDirNotFound)
}
fn try_install_with_privileges(&self, deb_path: &Path) -> Result<()> {
// 1. First try using pkexec (graphical sudo prompt)
if let Ok(status) = std::process::Command::new("pkexec")
.arg("dpkg")
.arg("-i")
.arg(deb_path)
.status()
{
if status.success() {
return Ok(());
}
}
// 2. Try zenity or kdialog for a graphical sudo experience
if let Ok(password) = self.get_password_graphically() {
if self.install_with_sudo(deb_path, &password)? {
return Ok(());
}
}
// 3. Final fallback: terminal sudo
let status = std::process::Command::new("sudo")
.arg("dpkg")
.arg("-i")
.arg(deb_path)
.status()?;
if status.success() {
Ok(())
} else {
Err(Error::DebInstallFailed)
}
}
fn get_password_graphically(&self) -> Result<String> {
// Try zenity first
let zenity_result = std::process::Command::new("zenity")
.args([
"--password",
"--title=Authentication Required",
"--text=Enter your password to install the update:",
])
.output();
if let Ok(output) = zenity_result {
if output.status.success() {
return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
}
}
// Fall back to kdialog if zenity fails or isn't available
let kdialog_result = std::process::Command::new("kdialog")
.args(["--password", "Enter your password to install the update:"])
.output();
if let Ok(output) = kdialog_result {
if output.status.success() {
return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string());
}
}
Err(Error::AuthenticationFailed)
}
fn install_with_sudo(&self, deb_path: &Path, password: &str) -> Result<bool> {
use std::io::Write;
use std::process::{Command, Stdio};
let mut child = Command::new("sudo")
.arg("-S") // read password from stdin
.arg("dpkg")
.arg("-i")
.arg(deb_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
if let Some(mut stdin) = child.stdin.take() {
// Write password to stdin
writeln!(stdin, "{}", password)?;
}
let status = child.wait()?;
Ok(status.success())
}
}
/// MacOS

@ -11,7 +11,7 @@
"devDependencies": {
"@tauri-apps/cli": "2.1.0",
"typescript": "^5.3.3",
"vite": "^5.4.7"
"vite": "^6.0.0"
},
"dependencies": {
"tauri-plugin-websocket-api": "link:..\\.."

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save