mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-04-21 11:26:15 +02:00
refactor: move deleted tauri APIs, prepare for next release (#355)
This commit is contained in:
committed by
GitHub
parent
937e6a5be6
commit
702b7b36bd
@@ -71,7 +71,8 @@
|
||||
|
||||
"dialog": {
|
||||
"path": "./plugins/dialog",
|
||||
"manager": "rust-disabled"
|
||||
"manager": "rust-disabled",
|
||||
"dependencies": ["fs"]
|
||||
},
|
||||
"dialog-js": {
|
||||
"path": "./plugins/dialog",
|
||||
@@ -107,7 +108,8 @@
|
||||
|
||||
"http": {
|
||||
"path": "./plugins/http",
|
||||
"manager": "rust-disabled"
|
||||
"manager": "rust-disabled",
|
||||
"dependencies": ["fs"]
|
||||
},
|
||||
"http-js": {
|
||||
"path": "./plugins/http",
|
||||
@@ -139,7 +141,8 @@
|
||||
|
||||
"persisted-scope": {
|
||||
"path": "./plugins/persisted-scope",
|
||||
"manager": "rust"
|
||||
"manager": "rust",
|
||||
"dependencies": ["fs"]
|
||||
},
|
||||
|
||||
"positioner": {
|
||||
|
||||
Generated
+16
-15
@@ -4960,15 +4960,13 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.0.0-alpha.8"
|
||||
source = "git+https://github.com/tauri-apps/tauri?branch=next#9a79dc085870e0c1a5df13481ff271b8c6cc3b78"
|
||||
source = "git+https://github.com/tauri-apps/tauri?branch=next#6d25c4d07fcf18c2a19ac4faa7d9bedd96d1a75f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes 1.4.0",
|
||||
"cocoa",
|
||||
"dirs-next",
|
||||
"embed_plist",
|
||||
"encoding_rs",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"glib",
|
||||
"glob",
|
||||
@@ -4976,7 +4974,6 @@ dependencies = [
|
||||
"heck 0.4.1",
|
||||
"http",
|
||||
"ico 0.2.0",
|
||||
"ignore",
|
||||
"infer 0.9.0",
|
||||
"jni",
|
||||
"libc",
|
||||
@@ -4995,7 +4992,6 @@ dependencies = [
|
||||
"serialize-to-javascript",
|
||||
"state",
|
||||
"swift-rs",
|
||||
"tar",
|
||||
"tauri-build",
|
||||
"tauri-macros",
|
||||
"tauri-runtime",
|
||||
@@ -5003,24 +4999,21 @@ dependencies = [
|
||||
"tauri-utils",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"time 0.3.20",
|
||||
"tokio",
|
||||
"url",
|
||||
"uuid",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows 0.44.0",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "2.0.0-alpha.4"
|
||||
source = "git+https://github.com/tauri-apps/tauri?branch=next#9a79dc085870e0c1a5df13481ff271b8c6cc3b78"
|
||||
source = "git+https://github.com/tauri-apps/tauri?branch=next#6d25c4d07fcf18c2a19ac4faa7d9bedd96d1a75f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
"filetime",
|
||||
"heck 0.4.1",
|
||||
"json-patch",
|
||||
"quote",
|
||||
@@ -5037,7 +5030,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "2.0.0-alpha.4"
|
||||
source = "git+https://github.com/tauri-apps/tauri?branch=next#9a79dc085870e0c1a5df13481ff271b8c6cc3b78"
|
||||
source = "git+https://github.com/tauri-apps/tauri?branch=next#6d25c4d07fcf18c2a19ac4faa7d9bedd96d1a75f"
|
||||
dependencies = [
|
||||
"base64 0.21.0",
|
||||
"brotli",
|
||||
@@ -5062,7 +5055,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "2.0.0-alpha.4"
|
||||
source = "git+https://github.com/tauri-apps/tauri?branch=next#9a79dc085870e0c1a5df13481ff271b8c6cc3b78"
|
||||
source = "git+https://github.com/tauri-apps/tauri?branch=next#6d25c4d07fcf18c2a19ac4faa7d9bedd96d1a75f"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
@@ -5147,6 +5140,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
@@ -5155,9 +5149,11 @@ name = "tauri-plugin-fs"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"glob",
|
||||
"serde",
|
||||
"tauri",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5198,6 +5194,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
@@ -5273,6 +5270,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
@@ -5373,8 +5371,10 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"base64 0.21.0",
|
||||
"dirs-next",
|
||||
"flate2",
|
||||
"futures-util",
|
||||
"http",
|
||||
"ignore",
|
||||
"minisign-verify",
|
||||
"mockito",
|
||||
"percent-encoding",
|
||||
@@ -5382,6 +5382,7 @@ dependencies = [
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tar",
|
||||
"tauri",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
@@ -5389,6 +5390,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-test",
|
||||
"url",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5447,7 +5449,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "0.13.0-alpha.4"
|
||||
source = "git+https://github.com/tauri-apps/tauri?branch=next#9a79dc085870e0c1a5df13481ff271b8c6cc3b78"
|
||||
source = "git+https://github.com/tauri-apps/tauri?branch=next#6d25c4d07fcf18c2a19ac4faa7d9bedd96d1a75f"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
@@ -5461,14 +5463,13 @@ dependencies = [
|
||||
"thiserror",
|
||||
"url",
|
||||
"uuid",
|
||||
"webview2-com",
|
||||
"windows 0.44.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "0.13.0-alpha.4"
|
||||
source = "git+https://github.com/tauri-apps/tauri?branch=next#9a79dc085870e0c1a5df13481ff271b8c6cc3b78"
|
||||
source = "git+https://github.com/tauri-apps/tauri?branch=next#6d25c4d07fcf18c2a19ac4faa7d9bedd96d1a75f"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"gtk",
|
||||
@@ -5488,7 +5489,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "2.0.0-alpha.4"
|
||||
source = "git+https://github.com/tauri-apps/tauri?branch=next#9a79dc085870e0c1a5df13481ff271b8c6cc3b78"
|
||||
source = "git+https://github.com/tauri-apps/tauri?branch=next#6d25c4d07fcf18c2a19ac4faa7d9bedd96d1a75f"
|
||||
dependencies = [
|
||||
"aes-gcm 0.10.1",
|
||||
"brotli",
|
||||
|
||||
Generated
-5252
File diff suppressed because it is too large
Load Diff
@@ -33,13 +33,12 @@ tauri-plugin-window = { path = "../../../plugins/window" }
|
||||
[dependencies.tauri]
|
||||
workspace = true
|
||||
features = [
|
||||
"api-all",
|
||||
"icon-ico",
|
||||
"icon-png",
|
||||
"isolation",
|
||||
"macos-private-api",
|
||||
"system-tray",
|
||||
"updater"
|
||||
"protocol-asset"
|
||||
]
|
||||
|
||||
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
|
||||
|
||||
@@ -46,47 +46,8 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tauri": {
|
||||
"pattern": {
|
||||
"use": "isolation",
|
||||
"options": {
|
||||
"dir": "../isolation-dist/"
|
||||
}
|
||||
},
|
||||
"macOSPrivateApi": true,
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"identifier": "com.tauri.api",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"windows": {
|
||||
"wix": {
|
||||
"language": {
|
||||
"en-US": {},
|
||||
"pt-BR": {
|
||||
"localePath": "locales/pt-BR.wxl"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK",
|
||||
"endpoints": [
|
||||
"https://tauri-update-server.vercel.app/update/{{target}}/{{current_version}}"
|
||||
]
|
||||
},
|
||||
"allowlist": {
|
||||
"all": true,
|
||||
"fs": {
|
||||
"fs": {
|
||||
"scope": {
|
||||
"allow": ["$APPDATA/db/**", "$DOWNLOAD/**", "$RESOURCE/**"],
|
||||
"deny": ["$APPDATA/db/*.stronghold"]
|
||||
@@ -117,15 +78,46 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"protocol": {
|
||||
"asset": true,
|
||||
"assetScope": {
|
||||
"allow": ["$APPDATA/db/**", "$RESOURCE/**"],
|
||||
"deny": ["$APPDATA/db/*.stronghold"]
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
"scope": ["http://localhost:3003"]
|
||||
},
|
||||
"updater": {
|
||||
"endpoints": [
|
||||
"https://tauri-update-server.vercel.app/update/{{target}}/{{current_version}}"
|
||||
]
|
||||
}
|
||||
},
|
||||
"tauri": {
|
||||
"pattern": {
|
||||
"use": "isolation",
|
||||
"options": {
|
||||
"dir": "../isolation-dist/"
|
||||
}
|
||||
},
|
||||
"macOSPrivateApi": true,
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"identifier": "com.tauri.api",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"windows": {
|
||||
"wix": {
|
||||
"language": {
|
||||
"en-US": {},
|
||||
"pt-BR": {
|
||||
"localePath": "locales/pt-BR.wxl"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK"
|
||||
}
|
||||
},
|
||||
"windows": [],
|
||||
@@ -136,7 +128,14 @@
|
||||
"img-src": "'self' asset: https://asset.localhost blob: data:",
|
||||
"style-src": "'unsafe-inline' 'self' https://fonts.googleapis.com"
|
||||
},
|
||||
"freezePrototype": true
|
||||
"freezePrototype": true,
|
||||
"assetProtocol": {
|
||||
"enable": true,
|
||||
"scope": {
|
||||
"allow": ["$APPDATA/db/**", "$RESOURCE/**"],
|
||||
"deny": ["$APPDATA/db/*.stronghold"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"systemTray": {
|
||||
"iconPath": "icons/tray_icon_with_transparency.png",
|
||||
|
||||
@@ -12,8 +12,7 @@ pub fn name<R: Runtime>(app: AppHandle<R>) -> String {
|
||||
|
||||
#[tauri::command]
|
||||
pub fn tauri_version() -> &'static str {
|
||||
// TODO: return actual tauri version with `tauri::VERSION`
|
||||
env!("CARGO_PKG_VERSION")
|
||||
tauri::VERSION
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -11,12 +11,11 @@ use config::{Arg, Config};
|
||||
pub use error::Error;
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
// TODO: use PluginApi#app when 2.0.0-alpha.9 is released
|
||||
pub struct Cli<R: Runtime>(PluginApi<R, Config>, AppHandle<R>);
|
||||
pub struct Cli<R: Runtime>(PluginApi<R, Config>);
|
||||
|
||||
impl<R: Runtime> Cli<R> {
|
||||
pub fn matches(&self) -> Result<parser::Matches> {
|
||||
parser::get_matches(self.0.config(), self.1.package_info())
|
||||
parser::get_matches(self.0.config(), self.0.app().package_info())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +38,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Config> {
|
||||
Builder::new("cli")
|
||||
.invoke_handler(tauri::generate_handler![cli_matches])
|
||||
.setup(|app, api| {
|
||||
app.manage(Cli(api, app.clone()));
|
||||
app.manage(Cli(api));
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
|
||||
Generated
-3584
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,7 @@ serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
log.workspace = true
|
||||
thiserror.workspace = true
|
||||
tauri-plugin-fs = { path = "../fs", version = "0.0.0" }
|
||||
|
||||
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
|
||||
glib = "0.16"
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{command, Manager, Runtime, State, Window};
|
||||
use tauri_plugin_fs::FsExt;
|
||||
|
||||
use crate::{Dialog, FileDialogBuilder, FileResponse, MessageDialogKind, Result};
|
||||
|
||||
@@ -114,16 +115,18 @@ pub(crate) async fn open<R: Runtime>(
|
||||
let folders = dialog_builder.blocking_pick_folders();
|
||||
if let Some(folders) = &folders {
|
||||
for folder in folders {
|
||||
window
|
||||
.fs_scope()
|
||||
.allow_directory(folder, options.recursive)?;
|
||||
if let Some(s) = window.try_fs_scope() {
|
||||
s.allow_directory(folder, options.recursive)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
OpenResponse::Folders(folders)
|
||||
} else {
|
||||
let folder = dialog_builder.blocking_pick_folder();
|
||||
if let Some(path) = &folder {
|
||||
window.fs_scope().allow_directory(path, options.recursive)?;
|
||||
if let Some(s) = window.try_fs_scope() {
|
||||
s.allow_directory(path, options.recursive)?;
|
||||
}
|
||||
}
|
||||
OpenResponse::Folder(folder)
|
||||
}
|
||||
@@ -134,14 +137,24 @@ pub(crate) async fn open<R: Runtime>(
|
||||
let files = dialog_builder.blocking_pick_files();
|
||||
if let Some(files) = &files {
|
||||
for file in files {
|
||||
window.fs_scope().allow_file(&file.path)?;
|
||||
if let Some(s) = window.try_fs_scope() {
|
||||
s.allow_file(&file.path)?;
|
||||
}
|
||||
window
|
||||
.state::<tauri::scope::Scopes>()
|
||||
.allow_file(&file.path)?;
|
||||
}
|
||||
}
|
||||
OpenResponse::Files(files)
|
||||
} else {
|
||||
let file = dialog_builder.blocking_pick_file();
|
||||
if let Some(file) = &file {
|
||||
window.fs_scope().allow_file(&file.path)?;
|
||||
if let Some(s) = window.try_fs_scope() {
|
||||
s.allow_file(&file.path)?;
|
||||
}
|
||||
window
|
||||
.state::<tauri::scope::Scopes>()
|
||||
.allow_file(&file.path)?;
|
||||
}
|
||||
OpenResponse::File(file)
|
||||
};
|
||||
@@ -177,7 +190,10 @@ pub(crate) async fn save<R: Runtime>(
|
||||
|
||||
let path = dialog_builder.blocking_save_file();
|
||||
if let Some(p) = &path {
|
||||
window.fs_scope().allow_file(p)?;
|
||||
if let Some(s) = window.try_fs_scope() {
|
||||
s.allow_file(p)?;
|
||||
}
|
||||
window.state::<tauri::scope::Scopes>().allow_file(p)?;
|
||||
}
|
||||
|
||||
Ok(path)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
|
||||
use serde::de::DeserializeOwned;
|
||||
use tauri::{plugin::PluginApi, AppHandle, Runtime};
|
||||
|
||||
@@ -101,6 +102,14 @@ impl From<MessageDialogKind> for rfd::MessageLevel {
|
||||
}
|
||||
}
|
||||
|
||||
struct WindowHandle(RawWindowHandle);
|
||||
|
||||
unsafe impl HasRawWindowHandle for WindowHandle {
|
||||
fn raw_window_handle(&self) -> RawWindowHandle {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> From<FileDialogBuilder<R>> for FileDialog {
|
||||
fn from(d: FileDialogBuilder<R>) -> Self {
|
||||
let mut builder = FileDialog::new();
|
||||
@@ -119,8 +128,8 @@ impl<R: Runtime> From<FileDialogBuilder<R>> for FileDialog {
|
||||
builder = builder.add_filter(&filter.name, &v);
|
||||
}
|
||||
#[cfg(desktop)]
|
||||
if let Some(_parent) = d.parent {
|
||||
// TODO builder = builder.set_parent(&parent);
|
||||
if let Some(parent) = d.parent {
|
||||
builder = builder.set_parent(&WindowHandle(parent));
|
||||
}
|
||||
|
||||
builder
|
||||
@@ -144,8 +153,8 @@ impl<R: Runtime> From<MessageDialogBuilder<R>> for rfd::MessageDialog {
|
||||
dialog = dialog.set_buttons(buttons);
|
||||
}
|
||||
|
||||
if let Some(_parent) = d.parent {
|
||||
// TODO dialog.set_parent(parent);
|
||||
if let Some(parent) = d.parent {
|
||||
dialog = dialog.set_parent(&WindowHandle(parent));
|
||||
}
|
||||
|
||||
dialog
|
||||
|
||||
@@ -21,6 +21,8 @@ pub enum Error {
|
||||
#[cfg(mobile)]
|
||||
#[error("File save dialog is not implemented on mobile")]
|
||||
FileSaveDialogNotImplemented,
|
||||
#[error(transparent)]
|
||||
Fs(#[from] tauri_plugin_fs::Error),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
|
||||
Generated
-3591
File diff suppressed because it is too large
Load Diff
@@ -14,3 +14,5 @@ serde.workspace = true
|
||||
tauri.workspace = true
|
||||
thiserror.workspace = true
|
||||
anyhow = "1"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
glob = "0.3"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::Scope;
|
||||
use anyhow::Context;
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use tauri::{
|
||||
path::{BaseDirectory, SafePathBuf},
|
||||
FsScope, Manager, Runtime, Window,
|
||||
Manager, Runtime, Window,
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -16,7 +17,7 @@ use std::{
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use crate::{Error, Result};
|
||||
use crate::{Error, FsExt, Result};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CommandError {
|
||||
@@ -120,7 +121,7 @@ pub fn write_file<R: Runtime>(
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ReadDirOptions<'a> {
|
||||
pub scope: Option<&'a FsScope>,
|
||||
pub scope: Option<&'a Scope>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -189,7 +190,7 @@ pub fn read_dir<R: Runtime>(
|
||||
&resolved_path,
|
||||
recursive,
|
||||
ReadDirOptions {
|
||||
scope: Some(&window.fs_scope()),
|
||||
scope: Some(window.fs_scope()),
|
||||
},
|
||||
)
|
||||
.with_context(|| format!("path: {}", resolved_path.display()))
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
pub scope: FsScope,
|
||||
}
|
||||
|
||||
/// Protocol scope definition.
|
||||
/// It is a list of glob patterns that restrict the API access from the webview.
|
||||
///
|
||||
/// Each 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`.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum FsScope {
|
||||
/// A list of paths that are allowed by this scope.
|
||||
AllowedPaths(Vec<PathBuf>),
|
||||
/// A complete scope configuration.
|
||||
Scope {
|
||||
/// A list of paths that are allowed by this scope.
|
||||
#[serde(default)]
|
||||
allow: Vec<PathBuf>,
|
||||
/// A list of paths that are not allowed by this scope.
|
||||
/// This gets precedence over the [`Self::Scope::allow`] list.
|
||||
#[serde(default)]
|
||||
deny: Vec<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for FsScope {
|
||||
fn default() -> Self {
|
||||
Self::AllowedPaths(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl FsScope {
|
||||
/// The list of allowed paths.
|
||||
pub fn allowed_paths(&self) -> &Vec<PathBuf> {
|
||||
match self {
|
||||
Self::AllowedPaths(p) => p,
|
||||
Self::Scope { allow, .. } => allow,
|
||||
}
|
||||
}
|
||||
|
||||
/// The list of forbidden paths.
|
||||
pub fn forbidden_paths(&self) -> Option<&Vec<PathBuf>> {
|
||||
match self {
|
||||
Self::AllowedPaths(_) => None,
|
||||
Self::Scope { deny, .. } => Some(deny),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@ pub enum Error {
|
||||
PathForbidden(PathBuf),
|
||||
#[error("failed to resolve path: {0}")]
|
||||
CannotResolvePath(tauri::path::Error),
|
||||
/// Invalid glob pattern.
|
||||
#[error("invalid glob pattern: {0}")]
|
||||
GlobPattern(#[from] glob::PatternError),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
|
||||
+51
-3
@@ -2,20 +2,40 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use config::FsScope;
|
||||
use tauri::{
|
||||
plugin::{Builder as PluginBuilder, TauriPlugin},
|
||||
Runtime,
|
||||
FileDropEvent, Manager, RunEvent, Runtime, WindowEvent,
|
||||
};
|
||||
|
||||
mod commands;
|
||||
mod config;
|
||||
mod error;
|
||||
mod scope;
|
||||
|
||||
pub use config::Config;
|
||||
pub use error::Error;
|
||||
pub use scope::{Event as ScopeEvent, Scope};
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
PluginBuilder::new("fs")
|
||||
pub trait FsExt<R: Runtime> {
|
||||
fn fs_scope(&self) -> &Scope;
|
||||
fn try_fs_scope(&self) -> Option<&Scope>;
|
||||
}
|
||||
|
||||
impl<R: Runtime, T: Manager<R>> FsExt<R> for T {
|
||||
fn fs_scope(&self) -> &Scope {
|
||||
self.state::<Scope>().inner()
|
||||
}
|
||||
|
||||
fn try_fs_scope(&self) -> Option<&Scope> {
|
||||
self.try_state::<Scope>().map(|s| s.inner())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<Config>> {
|
||||
PluginBuilder::<R, Option<Config>>::new("fs")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::read_file,
|
||||
commands::read_text_file,
|
||||
@@ -29,5 +49,33 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
commands::exists,
|
||||
commands::metadata
|
||||
])
|
||||
.setup(|app: &tauri::AppHandle<R>, api| {
|
||||
let default_scope = FsScope::default();
|
||||
app.manage(Scope::new(
|
||||
app,
|
||||
api.config()
|
||||
.as_ref()
|
||||
.map(|c| &c.scope)
|
||||
.unwrap_or(&default_scope),
|
||||
)?);
|
||||
Ok(())
|
||||
})
|
||||
.on_event(|app, event| {
|
||||
if let RunEvent::WindowEvent {
|
||||
label: _,
|
||||
event: WindowEvent::FileDrop(FileDropEvent::Dropped(paths)),
|
||||
..
|
||||
} = event
|
||||
{
|
||||
let scope = app.fs_scope();
|
||||
for path in paths {
|
||||
if path.is_file() {
|
||||
let _ = scope.allow_file(path);
|
||||
} else {
|
||||
let _ = scope.allow_directory(path, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt,
|
||||
path::{Path, PathBuf, MAIN_SEPARATOR},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crate::config::FsScope;
|
||||
pub use glob::Pattern;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{Manager, Runtime};
|
||||
|
||||
/// Scope change event.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Event {
|
||||
/// A path has been allowed.
|
||||
PathAllowed(PathBuf),
|
||||
/// A path has been forbidden.
|
||||
PathForbidden(PathBuf),
|
||||
}
|
||||
|
||||
type EventListener = Box<dyn Fn(&Event) + Send>;
|
||||
|
||||
/// Scope for filesystem access.
|
||||
#[derive(Clone)]
|
||||
pub struct Scope {
|
||||
allowed_patterns: Arc<Mutex<HashSet<Pattern>>>,
|
||||
forbidden_patterns: Arc<Mutex<HashSet<Pattern>>>,
|
||||
event_listeners: Arc<Mutex<HashMap<Uuid, EventListener>>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Scope {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Scope")
|
||||
.field(
|
||||
"allowed_patterns",
|
||||
&self
|
||||
.allowed_patterns
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|p| p.as_str())
|
||||
.collect::<Vec<&str>>(),
|
||||
)
|
||||
.field(
|
||||
"forbidden_patterns",
|
||||
&self
|
||||
.forbidden_patterns
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|p| p.as_str())
|
||||
.collect::<Vec<&str>>(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn push_pattern<P: AsRef<Path>, F: Fn(&str) -> Result<Pattern, glob::PatternError>>(
|
||||
list: &mut HashSet<Pattern>,
|
||||
pattern: P,
|
||||
f: F,
|
||||
) -> crate::Result<()> {
|
||||
let path: PathBuf = pattern.as_ref().components().collect();
|
||||
list.insert(f(&path.to_string_lossy())?);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if let Ok(p) = std::fs::canonicalize(&path) {
|
||||
list.insert(f(&p.to_string_lossy())?);
|
||||
} else {
|
||||
list.insert(f(&format!("\\\\?\\{}", path.display()))?);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
/// Creates a new scope from a `FsAllowlistScope` configuration.
|
||||
pub(crate) fn new<R: Runtime, M: Manager<R>>(
|
||||
manager: &M,
|
||||
scope: &FsScope,
|
||||
) -> crate::Result<Self> {
|
||||
let mut allowed_patterns = HashSet::new();
|
||||
for path in scope.allowed_paths() {
|
||||
if let Ok(path) = manager.path().parse(path) {
|
||||
push_pattern(&mut allowed_patterns, path, Pattern::new)?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut forbidden_patterns = HashSet::new();
|
||||
if let Some(forbidden_paths) = scope.forbidden_paths() {
|
||||
for path in forbidden_paths {
|
||||
if let Ok(path) = manager.path().parse(path) {
|
||||
push_pattern(&mut forbidden_patterns, path, Pattern::new)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
allowed_patterns: Arc::new(Mutex::new(allowed_patterns)),
|
||||
forbidden_patterns: Arc::new(Mutex::new(forbidden_patterns)),
|
||||
event_listeners: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// The list of allowed patterns.
|
||||
pub fn allowed_patterns(&self) -> HashSet<Pattern> {
|
||||
self.allowed_patterns.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
/// The list of forbidden patterns.
|
||||
pub fn forbidden_patterns(&self) -> HashSet<Pattern> {
|
||||
self.forbidden_patterns.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Listen to an event on this scope.
|
||||
pub fn listen<F: Fn(&Event) + Send + 'static>(&self, f: F) -> Uuid {
|
||||
let id = Uuid::new_v4();
|
||||
self.event_listeners.lock().unwrap().insert(id, Box::new(f));
|
||||
id
|
||||
}
|
||||
|
||||
fn trigger(&self, event: Event) {
|
||||
let listeners = self.event_listeners.lock().unwrap();
|
||||
let handlers = listeners.values();
|
||||
for listener in handlers {
|
||||
listener(&event);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) -> crate::Result<()> {
|
||||
let path = path.as_ref();
|
||||
{
|
||||
let mut list = self.allowed_patterns.lock().unwrap();
|
||||
|
||||
// allow the directory to be read
|
||||
push_pattern(&mut list, path, escaped_pattern)?;
|
||||
// allow its files and subdirectories to be read
|
||||
push_pattern(&mut list, path, |p| {
|
||||
escaped_pattern_with(p, if recursive { "**" } else { "*" })
|
||||
})?;
|
||||
}
|
||||
self.trigger(Event::PathAllowed(path.to_path_buf()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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) -> crate::Result<()> {
|
||||
let path = path.as_ref();
|
||||
push_pattern(
|
||||
&mut self.allowed_patterns.lock().unwrap(),
|
||||
path,
|
||||
escaped_pattern,
|
||||
)?;
|
||||
self.trigger(Event::PathAllowed(path.to_path_buf()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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) -> crate::Result<()> {
|
||||
let path = path.as_ref();
|
||||
{
|
||||
let mut list = self.forbidden_patterns.lock().unwrap();
|
||||
|
||||
// allow the directory to be read
|
||||
push_pattern(&mut list, path, escaped_pattern)?;
|
||||
// allow its files and subdirectories to be read
|
||||
push_pattern(&mut list, path, |p| {
|
||||
escaped_pattern_with(p, if recursive { "**" } else { "*" })
|
||||
})?;
|
||||
}
|
||||
self.trigger(Event::PathForbidden(path.to_path_buf()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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) -> crate::Result<()> {
|
||||
let path = path.as_ref();
|
||||
push_pattern(
|
||||
&mut self.forbidden_patterns.lock().unwrap(),
|
||||
path,
|
||||
escaped_pattern,
|
||||
)?;
|
||||
self.trigger(Event::PathForbidden(path.to_path_buf()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Determines if the given path is allowed on this scope.
|
||||
pub fn is_allowed<P: AsRef<Path>>(&self, path: P) -> bool {
|
||||
let path = path.as_ref();
|
||||
let path = if !path.exists() {
|
||||
crate::Result::Ok(path.to_path_buf())
|
||||
} else {
|
||||
std::fs::canonicalize(path).map_err(Into::into)
|
||||
};
|
||||
|
||||
if let Ok(path) = path {
|
||||
let path: PathBuf = path.components().collect();
|
||||
let options = 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,
|
||||
// dotfiles are not supposed to be exposed by default
|
||||
#[cfg(unix)]
|
||||
require_literal_leading_dot: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let forbidden = self
|
||||
.forbidden_patterns
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|p| p.matches_path_with(&path, options));
|
||||
|
||||
if forbidden {
|
||||
false
|
||||
} else {
|
||||
let allowed = self
|
||||
.allowed_patterns
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|p| p.matches_path_with(&path, options));
|
||||
allowed
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn escaped_pattern(p: &str) -> Result<Pattern, glob::PatternError> {
|
||||
Pattern::new(&glob::Pattern::escape(p))
|
||||
}
|
||||
|
||||
fn escaped_pattern_with(p: &str, append: &str) -> Result<Pattern, glob::PatternError> {
|
||||
Pattern::new(&format!(
|
||||
"{}{}{append}",
|
||||
glob::Pattern::escape(p),
|
||||
MAIN_SEPARATOR
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Scope;
|
||||
|
||||
fn new_scope() -> Scope {
|
||||
Scope {
|
||||
allowed_patterns: Default::default(),
|
||||
forbidden_patterns: Default::default(),
|
||||
event_listeners: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_is_escaped() {
|
||||
let scope = new_scope();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
scope.allow_directory("/home/tauri/**", false).unwrap();
|
||||
assert!(scope.is_allowed("/home/tauri/**"));
|
||||
assert!(scope.is_allowed("/home/tauri/**/file"));
|
||||
assert!(!scope.is_allowed("/home/tauri/anyfile"));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
scope.allow_directory("C:\\home\\tauri\\**", false).unwrap();
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
|
||||
}
|
||||
|
||||
let scope = new_scope();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
scope.allow_file("/home/tauri/**").unwrap();
|
||||
assert!(scope.is_allowed("/home/tauri/**"));
|
||||
assert!(!scope.is_allowed("/home/tauri/**/file"));
|
||||
assert!(!scope.is_allowed("/home/tauri/anyfile"));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
scope.allow_file("C:\\home\\tauri\\**").unwrap();
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\anyfile"));
|
||||
}
|
||||
|
||||
let scope = new_scope();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
scope.allow_directory("/home/tauri", true).unwrap();
|
||||
scope.forbid_directory("/home/tauri/**", false).unwrap();
|
||||
assert!(!scope.is_allowed("/home/tauri/**"));
|
||||
assert!(!scope.is_allowed("/home/tauri/**/file"));
|
||||
assert!(scope.is_allowed("/home/tauri/**/inner/file"));
|
||||
assert!(scope.is_allowed("/home/tauri/inner/folder/anyfile"));
|
||||
assert!(scope.is_allowed("/home/tauri/anyfile"));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
scope.allow_directory("C:\\home\\tauri", true).unwrap();
|
||||
scope
|
||||
.forbid_directory("C:\\home\\tauri\\**", false)
|
||||
.unwrap();
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\inner\\folder\\anyfile"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
|
||||
}
|
||||
|
||||
let scope = new_scope();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
scope.allow_directory("/home/tauri", true).unwrap();
|
||||
scope.forbid_file("/home/tauri/**").unwrap();
|
||||
assert!(!scope.is_allowed("/home/tauri/**"));
|
||||
assert!(scope.is_allowed("/home/tauri/**/file"));
|
||||
assert!(scope.is_allowed("/home/tauri/**/inner/file"));
|
||||
assert!(scope.is_allowed("/home/tauri/anyfile"));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
scope.allow_directory("C:\\home\\tauri", true).unwrap();
|
||||
scope.forbid_file("C:\\home\\tauri\\**").unwrap();
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\**"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**\\file"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
|
||||
}
|
||||
|
||||
let scope = new_scope();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
scope.allow_directory("/home/tauri", false).unwrap();
|
||||
assert!(scope.is_allowed("/home/tauri/**"));
|
||||
assert!(!scope.is_allowed("/home/tauri/**/file"));
|
||||
assert!(!scope.is_allowed("/home/tauri/**/inner/file"));
|
||||
assert!(scope.is_allowed("/home/tauri/anyfile"));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
scope.allow_directory("C:\\home\\tauri", false).unwrap();
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\**"));
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\file"));
|
||||
assert!(!scope.is_allowed("C:\\home\\tauri\\**\\inner\\file"));
|
||||
assert!(scope.is_allowed("C:\\home\\tauri\\anyfile"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
-3874
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
thiserror.workspace = true
|
||||
tauri-plugin-fs = { path = "../fs", version = "0.0.0" }
|
||||
glob = "0.3"
|
||||
rand = "0.8"
|
||||
bytes = { version = "1", features = [ "serde" ] }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use tauri::{path::SafePathBuf, AppHandle, Runtime, State};
|
||||
use tauri_plugin_fs::FsExt;
|
||||
|
||||
use crate::{ClientId, Http};
|
||||
|
||||
@@ -38,7 +39,6 @@ pub async fn request<R: Runtime>(
|
||||
client_id: ClientId,
|
||||
options: Box<HttpRequestBuilder>,
|
||||
) -> super::Result<ResponseData> {
|
||||
use crate::Manager;
|
||||
if http.scope.is_allowed(&options.url) {
|
||||
let client = http
|
||||
.clients
|
||||
@@ -55,7 +55,12 @@ pub async fn request<R: Runtime>(
|
||||
..
|
||||
} = value
|
||||
{
|
||||
if SafePathBuf::new(path.clone()).is_err() || !app.fs_scope().is_allowed(path) {
|
||||
if SafePathBuf::new(path.clone()).is_err()
|
||||
|| !app
|
||||
.try_fs_scope()
|
||||
.map(|s| s.is_allowed(path))
|
||||
.unwrap_or_default()
|
||||
{
|
||||
return Err(crate::Error::PathNotAllowed(path.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
use reqwest::Url;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
pub scope: HttpAllowlistScope,
|
||||
}
|
||||
|
||||
/// HTTP API scope definition.
|
||||
/// It is a list of URLs that can be accessed by the webview when using the HTTP APIs.
|
||||
/// The scoped URL is matched against the request URL using a glob pattern.
|
||||
///
|
||||
/// Examples:
|
||||
/// - "https://**": allows all HTTPS urls
|
||||
/// - "https://*.github.com/tauri-apps/tauri": allows any subdomain of "github.com" with the "tauri-apps/api" path
|
||||
/// - "https://myapi.service.com/users/*": allows access to any URLs that begins with "https://myapi.service.com/users/"
|
||||
#[allow(rustdoc::bare_urls)]
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)]
|
||||
pub struct HttpAllowlistScope(pub Vec<Url>);
|
||||
+12
-4
@@ -1,3 +1,4 @@
|
||||
use config::{Config, HttpAllowlistScope};
|
||||
pub use reqwest as client;
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
@@ -7,6 +8,7 @@ use tauri::{
|
||||
use std::{collections::HashMap, sync::Mutex};
|
||||
|
||||
mod commands;
|
||||
mod config;
|
||||
mod error;
|
||||
mod scope;
|
||||
|
||||
@@ -33,18 +35,24 @@ impl<R: Runtime, T: Manager<R>> HttpExt<R> for T {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("http")
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<Config>> {
|
||||
Builder::<R, Option<Config>>::new("http")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::create_client,
|
||||
commands::drop_client,
|
||||
commands::request
|
||||
])
|
||||
.setup(|app, _api| {
|
||||
.setup(|app, api| {
|
||||
let default_scope = HttpAllowlistScope::default();
|
||||
app.manage(Http {
|
||||
app: app.clone(),
|
||||
clients: Default::default(),
|
||||
scope: scope::Scope::new(&app.config().tauri.allowlist.http.scope),
|
||||
scope: scope::Scope::new(
|
||||
api.config()
|
||||
.as_ref()
|
||||
.map(|c| &c.scope)
|
||||
.unwrap_or(&default_scope),
|
||||
),
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::config::HttpAllowlistScope;
|
||||
use glob::Pattern;
|
||||
use reqwest::Url;
|
||||
use tauri::utils::config::HttpAllowlistScope;
|
||||
|
||||
/// Scope for filesystem access.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -38,7 +38,7 @@ impl Scope {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tauri::utils::config::HttpAllowlistScope;
|
||||
use crate::config::HttpAllowlistScope;
|
||||
|
||||
#[test]
|
||||
fn is_allowed() {
|
||||
|
||||
@@ -17,6 +17,4 @@ log.workspace = true
|
||||
thiserror.workspace = true
|
||||
aho-corasick = "1.0"
|
||||
bincode = "1"
|
||||
|
||||
[features]
|
||||
protocol-asset = [ "tauri/protocol-asset" ]
|
||||
tauri-plugin-fs = { path = "../fs", version = "0.0.0" }
|
||||
|
||||
@@ -6,8 +6,9 @@ use aho_corasick::AhoCorasick;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
AppHandle, FsScopeEvent, Manager, Runtime,
|
||||
AppHandle, Manager, Runtime,
|
||||
};
|
||||
use tauri_plugin_fs::{FsExt, ScopeEvent as FsScopeEvent};
|
||||
|
||||
use std::{
|
||||
fs::{create_dir_all, File},
|
||||
@@ -59,45 +60,45 @@ fn fix_pattern(ac: &AhoCorasick, s: &str) -> String {
|
||||
}
|
||||
|
||||
fn save_scopes<R: Runtime>(app: &AppHandle<R>, app_dir: &Path, scope_state_path: &Path) {
|
||||
let fs_scope = app.fs_scope();
|
||||
if let Some(fs_scope) = app.try_fs_scope() {
|
||||
let scope = Scope {
|
||||
allowed_paths: fs_scope
|
||||
.allowed_patterns()
|
||||
.into_iter()
|
||||
.map(|p| p.to_string())
|
||||
.collect(),
|
||||
forbidden_patterns: fs_scope
|
||||
.forbidden_patterns()
|
||||
.into_iter()
|
||||
.map(|p| p.to_string())
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let scope = Scope {
|
||||
allowed_paths: fs_scope
|
||||
.allowed_patterns()
|
||||
.into_iter()
|
||||
.map(|p| p.to_string())
|
||||
.collect(),
|
||||
forbidden_patterns: fs_scope
|
||||
.forbidden_patterns()
|
||||
.into_iter()
|
||||
.map(|p| p.to_string())
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let _ = create_dir_all(app_dir)
|
||||
.and_then(|_| File::create(scope_state_path))
|
||||
.map_err(Error::Io)
|
||||
.and_then(|mut f| {
|
||||
f.write_all(&bincode::serialize(&scope).map_err(Error::from)?)
|
||||
.map_err(Into::into)
|
||||
});
|
||||
let _ = create_dir_all(app_dir)
|
||||
.and_then(|_| File::create(scope_state_path))
|
||||
.map_err(Error::Io)
|
||||
.and_then(|mut f| {
|
||||
f.write_all(&bincode::serialize(&scope).map_err(Error::from)?)
|
||||
.map_err(Into::into)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("persisted-scope")
|
||||
.setup(|app, _api| {
|
||||
let fs_scope = app.fs_scope();
|
||||
#[cfg(feature = "protocol-asset")]
|
||||
let asset_protocol_scope = app.asset_protocol_scope();
|
||||
let fs_scope = app.try_fs_scope();
|
||||
let core_scopes = app.state::<tauri::scope::Scopes>();
|
||||
let app = app.clone();
|
||||
let app_dir = app.path().app_data_dir();
|
||||
|
||||
if let Ok(app_dir) = app_dir {
|
||||
let scope_state_path = app_dir.join(SCOPE_STATE_FILENAME);
|
||||
|
||||
let _ = fs_scope.forbid_file(&scope_state_path);
|
||||
#[cfg(feature = "protocol-asset")]
|
||||
let _ = asset_protocol_scope.forbid_file(&scope_state_path);
|
||||
if let Some(s) = fs_scope {
|
||||
let _ = s.forbid_file(&scope_state_path);
|
||||
}
|
||||
let _ = core_scopes.forbid_file(&scope_state_path);
|
||||
|
||||
// We're trying to fix broken .persisted-scope files seamlessly, so we'll be running this on the values read on the saved file.
|
||||
// We will still save some semi-broken values because the scope events are quite spammy and we don't want to reduce runtime performance any further.
|
||||
@@ -111,16 +112,18 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
for allowed in &scope.allowed_paths {
|
||||
let allowed = fix_pattern(&ac, allowed);
|
||||
|
||||
let _ = fs_scope.allow_file(&allowed);
|
||||
#[cfg(feature = "protocol-asset")]
|
||||
let _ = asset_protocol_scope.allow_file(&allowed);
|
||||
if let Some(s) = fs_scope {
|
||||
let _ = s.allow_file(&allowed);
|
||||
}
|
||||
let _ = core_scopes.allow_file(&allowed);
|
||||
}
|
||||
for forbidden in &scope.forbidden_patterns {
|
||||
let forbidden = fix_pattern(&ac, forbidden);
|
||||
|
||||
let _ = fs_scope.forbid_file(&forbidden);
|
||||
#[cfg(feature = "protocol-asset")]
|
||||
let _ = asset_protocol_scope.forbid_file(&forbidden);
|
||||
if let Some(s) = fs_scope {
|
||||
let _ = s.forbid_file(&forbidden);
|
||||
}
|
||||
let _ = core_scopes.forbid_file(&forbidden);
|
||||
}
|
||||
|
||||
// Manually save the fixed scopes to disk once.
|
||||
@@ -128,11 +131,13 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
save_scopes(&app, &app_dir, &scope_state_path);
|
||||
}
|
||||
|
||||
fs_scope.listen(move |event| {
|
||||
if let FsScopeEvent::PathAllowed(_) = event {
|
||||
save_scopes(&app, &app_dir, &scope_state_path);
|
||||
}
|
||||
});
|
||||
if let Some(s) = fs_scope {
|
||||
s.listen(move |event| {
|
||||
if let FsScopeEvent::PathAllowed(_) = event {
|
||||
save_scopes(&app, &app_dir, &scope_state_path);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{de::Error as DeError, Deserialize, Deserializer};
|
||||
|
||||
/// Allowlist for the shell APIs.
|
||||
///
|
||||
/// See more: https://tauri.app/v1/api/config#shellallowlistconfig
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
/// Access scope for the binary execution APIs.
|
||||
/// Sidecars are automatically enabled.
|
||||
#[serde(default)]
|
||||
pub scope: ShellAllowlistScope,
|
||||
/// Open URL with the user's default application.
|
||||
#[serde(default)]
|
||||
pub open: ShellAllowlistOpen,
|
||||
}
|
||||
|
||||
/// A command allowed to be executed by the webview API.
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct ShellAllowedCommand {
|
||||
/// The name for this allowed shell command configuration.
|
||||
///
|
||||
/// This name will be used inside of the webview API to call this command along with
|
||||
/// any specified arguments.
|
||||
pub name: String,
|
||||
|
||||
/// The command name.
|
||||
/// It 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`.
|
||||
// use default just so the schema doesn't flag it as required
|
||||
pub command: PathBuf,
|
||||
|
||||
/// The allowed arguments for the command execution.
|
||||
pub args: ShellAllowedArgs,
|
||||
|
||||
/// If this command is a sidecar command.
|
||||
pub sidecar: bool,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ShellAllowedCommand {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct InnerShellAllowedCommand {
|
||||
name: String,
|
||||
#[serde(rename = "cmd")]
|
||||
command: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
args: ShellAllowedArgs,
|
||||
#[serde(default)]
|
||||
sidecar: bool,
|
||||
}
|
||||
|
||||
let config = InnerShellAllowedCommand::deserialize(deserializer)?;
|
||||
|
||||
if !config.sidecar && config.command.is_none() {
|
||||
return Err(DeError::custom(
|
||||
"The shell scope `command` value is required.",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(ShellAllowedCommand {
|
||||
name: config.name,
|
||||
command: config.command.unwrap_or_default(),
|
||||
args: config.args,
|
||||
sidecar: config.sidecar,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of command arguments allowed to be executed by the webview API.
|
||||
///
|
||||
/// A value of `true` will allow any arguments to be passed to the command. `false` will disable all
|
||||
/// arguments. A list of [`ShellAllowedArg`] will set those arguments as the only valid arguments to
|
||||
/// be passed to the attached command configuration.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
#[non_exhaustive]
|
||||
pub enum ShellAllowedArgs {
|
||||
/// Use a simple boolean to allow all or disable all arguments to this command configuration.
|
||||
Flag(bool),
|
||||
|
||||
/// A specific set of [`ShellAllowedArg`] that are valid to call for the command configuration.
|
||||
List(Vec<ShellAllowedArg>),
|
||||
}
|
||||
|
||||
impl Default for ShellAllowedArgs {
|
||||
fn default() -> Self {
|
||||
Self::Flag(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// A command argument allowed to be executed by the webview API.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
#[non_exhaustive]
|
||||
pub enum ShellAllowedArg {
|
||||
/// A non-configurable argument that is passed to the command in the order it was specified.
|
||||
Fixed(String),
|
||||
|
||||
/// A variable that is set while calling the command from the webview API.
|
||||
///
|
||||
Var {
|
||||
/// [regex] validator to require passed values to conform to an expected input.
|
||||
///
|
||||
/// This will require the argument value passed to this variable to match the `validator` regex
|
||||
/// before it will be executed.
|
||||
///
|
||||
/// [regex]: https://docs.rs/regex/latest/regex/#syntax
|
||||
validator: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Shell scope definition.
|
||||
/// It is a list of command names and associated CLI arguments that restrict the API access from the webview.
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)]
|
||||
|
||||
pub struct ShellAllowlistScope(pub Vec<ShellAllowedCommand>);
|
||||
|
||||
/// Defines the `shell > open` api scope.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
#[non_exhaustive]
|
||||
pub enum ShellAllowlistOpen {
|
||||
/// If the shell open API should be enabled.
|
||||
///
|
||||
/// If enabled, the default validation regex (`^((mailto:\w+)|(tel:\w+)|(https?://\w+)).+`) is used.
|
||||
Flag(bool),
|
||||
|
||||
/// Enable the shell open API, with a custom regex that the opened path must match against.
|
||||
///
|
||||
/// If using a custom regex to support a non-http(s) schema, care should be used to prevent values
|
||||
/// that allow flag-like strings to pass validation. e.g. `--enable-debugging`, `-i`, `/R`.
|
||||
Validate(String),
|
||||
}
|
||||
|
||||
impl Default for ShellAllowlistOpen {
|
||||
fn default() -> Self {
|
||||
Self::Flag(false)
|
||||
}
|
||||
}
|
||||
@@ -8,16 +8,17 @@ use regex::Regex;
|
||||
use scope::{Scope, ScopeAllowedCommand, ScopeConfig};
|
||||
use tauri::{
|
||||
plugin::{Builder, TauriPlugin},
|
||||
utils::config::{ShellAllowedArg, ShellAllowedArgs, ShellAllowlistOpen, ShellAllowlistScope},
|
||||
AppHandle, Manager, RunEvent, Runtime,
|
||||
};
|
||||
|
||||
mod commands;
|
||||
mod config;
|
||||
mod error;
|
||||
mod open;
|
||||
pub mod process;
|
||||
mod scope;
|
||||
|
||||
use config::{Config, ShellAllowedArg, ShellAllowedArgs, ShellAllowlistOpen, ShellAllowlistScope};
|
||||
pub use error::Error;
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
type ChildStore = Arc<Mutex<HashMap<u32, CommandChild>>>;
|
||||
@@ -61,25 +62,21 @@ impl<R: Runtime, T: Manager<R>> ShellExt<R> for T {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("shell")
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<Config>> {
|
||||
Builder::<R, Option<Config>>::new("shell")
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::execute,
|
||||
commands::stdin_write,
|
||||
commands::kill,
|
||||
commands::open
|
||||
])
|
||||
.setup(|app, _api| {
|
||||
.setup(|app, api| {
|
||||
let default_config = Config::default();
|
||||
let config = api.config().as_ref().unwrap_or(&default_config);
|
||||
app.manage(Shell {
|
||||
app: app.clone(),
|
||||
children: Default::default(),
|
||||
scope: Scope::new(
|
||||
app,
|
||||
shell_scope(
|
||||
app.config().tauri.allowlist.shell.scope.clone(),
|
||||
&app.config().tauri.allowlist.shell.open,
|
||||
),
|
||||
),
|
||||
scope: Scope::new(app, shell_scope(config.scope.clone(), &config.open)),
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
@@ -111,7 +108,6 @@ fn shell_scope(scope: ShellAllowlistScope, open: &ShellAllowlistOpen) -> ScopeCo
|
||||
Regex::new(validator).unwrap_or_else(|e| panic!("invalid regex {validator}: {e}"));
|
||||
Some(validator)
|
||||
}
|
||||
_ => panic!("unknown shell open format, unable to prepare"),
|
||||
};
|
||||
|
||||
ScopeConfig {
|
||||
@@ -136,11 +132,9 @@ fn get_allowed_clis(scope: ShellAllowlistScope) -> HashMap<String, ScopeAllowedC
|
||||
.unwrap_or_else(|e| panic!("invalid regex {validator}: {e}"));
|
||||
scope::ScopeAllowedArg::Var { validator }
|
||||
}
|
||||
_ => panic!("unknown shell scope arg, unable to prepare"),
|
||||
});
|
||||
Some(list.collect())
|
||||
}
|
||||
_ => panic!("unknown shell scope command, unable to prepare"),
|
||||
};
|
||||
|
||||
(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -40,12 +40,6 @@
|
||||
"timestampUrl": ""
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
},
|
||||
"allowlist": {
|
||||
"all": true
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"title": "app",
|
||||
|
||||
Generated
-4184
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@ authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tauri = { workspace = true, features = ["updater", "fs-extract-api"] }
|
||||
tauri.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
@@ -23,6 +23,10 @@ percent-encoding = "2"
|
||||
semver = { version = "1", features = [ "serde" ] }
|
||||
futures-util = "0.3"
|
||||
tempfile = "3"
|
||||
flate2 = "1"
|
||||
tar = "0.4"
|
||||
ignore = "0.4"
|
||||
zip = { version = "0.6", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
mockito = "0.31"
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use url::Url;
|
||||
|
||||
/// Updater configuration.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub endpoints: Vec<UpdaterEndpoint>,
|
||||
/// Additional arguments given to the NSIS or WiX installer.
|
||||
#[serde(default, alias = "installer-args")]
|
||||
pub installer_args: Vec<String>,
|
||||
}
|
||||
|
||||
/// A URL to an updater server.
|
||||
///
|
||||
/// The URL must use the `https` scheme on production.
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct UpdaterEndpoint(pub Url);
|
||||
|
||||
impl std::fmt::Display for UpdaterEndpoint {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for UpdaterEndpoint {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let url = Url::deserialize(deserializer)?;
|
||||
#[cfg(all(not(debug_assertions), not(feature = "schema")))]
|
||||
{
|
||||
if url.scheme() != "https" {
|
||||
return Err(serde::de::Error::custom(
|
||||
"The configured updater endpoint must use the `https` protocol.",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(Self(url))
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,12 @@ pub enum Error {
|
||||
#[cfg(target_os = "linux")]
|
||||
#[error("temp directory is not on the same mount point as the AppImage")]
|
||||
TempDirNotOnSameMountPoint,
|
||||
/// The path StripPrefixError error.
|
||||
#[error("Path Error: {0}")]
|
||||
PathPrefix(#[from] std::path::StripPrefixError),
|
||||
/// Ignore error.
|
||||
#[error("failed to walkdir: {0}")]
|
||||
Ignore(#[from] ignore::Error),
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
|
||||
@@ -6,15 +6,18 @@ use tauri::{
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
mod commands;
|
||||
mod config;
|
||||
mod error;
|
||||
mod updater;
|
||||
|
||||
pub use config::Config;
|
||||
pub use error::Error;
|
||||
pub use updater::*;
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
struct UpdaterState {
|
||||
target: Option<String>,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
struct PendingUpdate<R: Runtime>(Mutex<Option<UpdateResponse<R>>>);
|
||||
@@ -22,6 +25,7 @@ struct PendingUpdate<R: Runtime>(Mutex<Option<UpdateResponse<R>>>);
|
||||
#[derive(Default)]
|
||||
pub struct Builder {
|
||||
target: Option<String>,
|
||||
installer_args: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Extension trait to use the updater on [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`].
|
||||
@@ -60,11 +64,26 @@ impl Builder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build<R: Runtime>(self) -> TauriPlugin<R> {
|
||||
pub fn installer_args<I, S>(mut self, args: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<String>,
|
||||
{
|
||||
self.installer_args
|
||||
.replace(args.into_iter().map(Into::into).collect());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build<R: Runtime>(self) -> TauriPlugin<R, Config> {
|
||||
let target = self.target;
|
||||
PluginBuilder::<R>::new("updater")
|
||||
.setup(move |app, _api| {
|
||||
app.manage(UpdaterState { target });
|
||||
let installer_args = self.installer_args;
|
||||
PluginBuilder::<R, Config>::new("updater")
|
||||
.setup(move |app, api| {
|
||||
let mut config = api.config().clone();
|
||||
if let Some(installer_args) = installer_args {
|
||||
config.installer_args = installer_args;
|
||||
}
|
||||
app.manage(UpdaterState { target, config });
|
||||
app.manage(PendingUpdate::<R>(Default::default()));
|
||||
Ok(())
|
||||
})
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#[cfg(desktop)]
|
||||
use super::{
|
||||
extract::{ArchiveFormat, Extract},
|
||||
move_file::Move,
|
||||
};
|
||||
use crate::{Error, Result};
|
||||
use base64::Engine;
|
||||
use futures_util::StreamExt;
|
||||
@@ -13,8 +18,6 @@ use minisign_verify::{PublicKey, Signature};
|
||||
use reqwest::ClientBuilder;
|
||||
use semver::Version;
|
||||
use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize};
|
||||
#[cfg(desktop)]
|
||||
use tauri::api::file::{ArchiveFormat, Extract, Move};
|
||||
use tauri::utils::{platform::current_exe, Env};
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
use time::OffsetDateTime;
|
||||
@@ -36,7 +39,7 @@ use std::{
|
||||
use std::ffi::OsStr;
|
||||
|
||||
#[cfg(all(desktop, not(target_os = "windows")))]
|
||||
use tauri::api::file::Compression;
|
||||
use super::extract::Compression;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::{
|
||||
@@ -607,6 +610,7 @@ impl<R: Runtime> Update<R> {
|
||||
&self.extract_path,
|
||||
self.with_elevated_task,
|
||||
&self.app.config(),
|
||||
&self.app.state::<UpdaterState>().config,
|
||||
)?;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
copy_files_and_run(archive_buffer, &self.extract_path)?;
|
||||
@@ -668,7 +672,7 @@ fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) ->
|
||||
// if something went wrong during the extraction, we should restore previous app
|
||||
if let Err(err) = entry.extract(extract_path) {
|
||||
Move::from_source(tmp_app_image).to_dest(extract_path)?;
|
||||
return Err(tauri::api::Error::Extract(err.to_string()));
|
||||
return Err(err);
|
||||
}
|
||||
// early finish we have everything we need here
|
||||
return Ok(true);
|
||||
@@ -706,6 +710,7 @@ fn copy_files_and_run<R: Read + Seek>(
|
||||
_extract_path: &Path,
|
||||
with_elevated_task: bool,
|
||||
config: &tauri::Config,
|
||||
updater_config: &crate::Config,
|
||||
) -> Result<()> {
|
||||
// FIXME: We need to create a memory buffer with the MSI and then run it.
|
||||
// (instead of extracting the MSI to a temp path)
|
||||
@@ -733,11 +738,11 @@ fn copy_files_and_run<R: Read + Seek>(
|
||||
// Run the EXE
|
||||
let mut installer = Command::new(found_path);
|
||||
if tauri::utils::config::WindowsUpdateInstallMode::Quiet
|
||||
== config.tauri.updater.windows.install_mode
|
||||
== config.tauri.bundle.updater.install_mode
|
||||
{
|
||||
installer.arg("/S");
|
||||
}
|
||||
installer.args(&config.tauri.updater.windows.installer_args);
|
||||
installer.args(&updater_config.installer_args);
|
||||
|
||||
installer.spawn().expect("installer failed to start");
|
||||
|
||||
@@ -793,17 +798,17 @@ fn copy_files_and_run<R: Read + Seek>(
|
||||
msi_path_arg.push(&found_path);
|
||||
msi_path_arg.push("\"\"\"");
|
||||
|
||||
let mut msiexec_args = config
|
||||
let mut msiexec_args = updater_config
|
||||
.tauri
|
||||
.bundle
|
||||
.updater
|
||||
.windows
|
||||
.install_mode
|
||||
.clone()
|
||||
.msiexec_args()
|
||||
.iter()
|
||||
.map(|p| p.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
msiexec_args.extend(config.tauri.updater.windows.installer_args.clone());
|
||||
msiexec_args.extend(updater_config.installer_args.clone());
|
||||
|
||||
// run the installer and relaunch the application
|
||||
let system_root = std::env::var("SYSTEMROOT");
|
||||
@@ -890,7 +895,7 @@ fn copy_files_and_run<R: Read + Seek>(archive_buffer: R, extract_path: &Path) ->
|
||||
}
|
||||
}
|
||||
Move::from_source(tmp_dir.path()).to_dest(extract_path)?;
|
||||
return Err(tauri::api::Error::Extract(err.to_string()));
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
extracted_files.push(extraction_path);
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fs,
|
||||
io::{self, Cursor, Read, Seek},
|
||||
path::{self, Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
/// The archive reader.
|
||||
#[derive(Debug)]
|
||||
pub enum ArchiveReader<R: Read + Seek> {
|
||||
/// A plain reader.
|
||||
Plain(R),
|
||||
/// A GZ- compressed reader (decoder).
|
||||
GzCompressed(Box<flate2::read::GzDecoder<R>>),
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Read for ArchiveReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
match self {
|
||||
Self::Plain(r) => r.read(buf),
|
||||
Self::GzCompressed(decoder) => decoder.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> ArchiveReader<R> {
|
||||
#[allow(dead_code)]
|
||||
fn get_mut(&mut self) -> &mut R {
|
||||
match self {
|
||||
Self::Plain(r) => r,
|
||||
Self::GzCompressed(decoder) => decoder.get_mut(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The supported archive formats.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum ArchiveFormat {
|
||||
/// Tar archive.
|
||||
Tar(Option<Compression>),
|
||||
/// Zip archive.
|
||||
#[allow(dead_code)]
|
||||
Zip,
|
||||
}
|
||||
|
||||
/// The supported compression types.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Compression {
|
||||
/// Gz compression (e.g. `.tar.gz` archives)
|
||||
Gz,
|
||||
}
|
||||
|
||||
/// The zip entry.
|
||||
pub struct ZipEntry {
|
||||
path: PathBuf,
|
||||
is_dir: bool,
|
||||
file_contents: Vec<u8>,
|
||||
}
|
||||
|
||||
/// A read-only view into an entry of an archive.
|
||||
#[non_exhaustive]
|
||||
pub enum Entry<'a, R: Read> {
|
||||
/// An entry of a tar archive.
|
||||
#[non_exhaustive]
|
||||
Tar(Box<tar::Entry<'a, R>>),
|
||||
/// An entry of a zip archive.
|
||||
#[non_exhaustive]
|
||||
#[allow(dead_code)]
|
||||
Zip(ZipEntry),
|
||||
}
|
||||
|
||||
impl<'a, R: Read> Entry<'a, R> {
|
||||
/// The entry path.
|
||||
pub fn path(&self) -> Result<Cow<'_, Path>> {
|
||||
match self {
|
||||
Self::Tar(e) => e.path().map_err(Into::into),
|
||||
Self::Zip(e) => Ok(Cow::Borrowed(&e.path)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract this entry into `into_path`.
|
||||
/// If it's a directory, the target will be created, if it's a file, it'll be extracted at this location.
|
||||
/// Note: You need to include the complete path, with file name and extension.
|
||||
pub fn extract(self, into_path: &path::Path) -> Result<()> {
|
||||
match self {
|
||||
Self::Tar(mut entry) => {
|
||||
// determine if it's a file or a directory
|
||||
if entry.header().entry_type() == tar::EntryType::Directory {
|
||||
// this is a directory, lets create it
|
||||
match fs::create_dir_all(into_path) {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
if e.kind() != io::ErrorKind::AlreadyExists {
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut out_file = fs::File::create(into_path)?;
|
||||
io::copy(&mut entry, &mut out_file)?;
|
||||
|
||||
// make sure we set permissions
|
||||
if let Ok(mode) = entry.header().mode() {
|
||||
set_perms(into_path, Some(&mut out_file), mode, true)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::Zip(entry) => {
|
||||
if entry.is_dir {
|
||||
// this is a directory, lets create it
|
||||
match fs::create_dir_all(into_path) {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
if e.kind() != io::ErrorKind::AlreadyExists {
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut out_file = fs::File::create(into_path)?;
|
||||
io::copy(&mut Cursor::new(entry.file_contents), &mut out_file)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The extract manager to retrieve files from archives.
|
||||
pub struct Extract<'a, R: Read + Seek> {
|
||||
reader: ArchiveReader<R>,
|
||||
archive_format: ArchiveFormat,
|
||||
tar_archive: Option<tar::Archive<&'a mut ArchiveReader<R>>>,
|
||||
}
|
||||
|
||||
impl<'a, R: std::fmt::Debug + Read + Seek> std::fmt::Debug for Extract<'a, R> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Extract")
|
||||
.field("reader", &self.reader)
|
||||
.field("archive_format", &self.archive_format)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R: Read + Seek> Extract<'a, R> {
|
||||
/// Create archive from reader.
|
||||
pub fn from_cursor(mut reader: R, archive_format: ArchiveFormat) -> Extract<'a, R> {
|
||||
if reader.rewind().is_err() {
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!("Could not seek to start of the file");
|
||||
}
|
||||
let compression = if let ArchiveFormat::Tar(compression) = archive_format {
|
||||
compression
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Extract {
|
||||
reader: match compression {
|
||||
Some(Compression::Gz) => {
|
||||
ArchiveReader::GzCompressed(Box::new(flate2::read::GzDecoder::new(reader)))
|
||||
}
|
||||
_ => ArchiveReader::Plain(reader),
|
||||
},
|
||||
archive_format,
|
||||
tar_archive: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the archive content.
|
||||
pub fn with_files<
|
||||
E: Into<Error>,
|
||||
F: FnMut(Entry<'_, &mut ArchiveReader<R>>) -> std::result::Result<bool, E>,
|
||||
>(
|
||||
&'a mut self,
|
||||
mut f: F,
|
||||
) -> Result<()> {
|
||||
match self.archive_format {
|
||||
ArchiveFormat::Tar(_) => {
|
||||
let archive = tar::Archive::new(&mut self.reader);
|
||||
self.tar_archive.replace(archive);
|
||||
for entry in self.tar_archive.as_mut().unwrap().entries()? {
|
||||
let entry = entry?;
|
||||
if entry.path().is_ok() {
|
||||
let stop = f(Entry::Tar(Box::new(entry))).map_err(Into::into)?;
|
||||
if stop {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ArchiveFormat::Zip => {
|
||||
#[cfg(feature = "fs-extract-api")]
|
||||
{
|
||||
let mut archive = zip::ZipArchive::new(self.reader.get_mut())?;
|
||||
let file_names = archive
|
||||
.file_names()
|
||||
.map(|f| f.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
for path in file_names {
|
||||
let mut zip_file = archive.by_name(&path)?;
|
||||
let is_dir = zip_file.is_dir();
|
||||
let mut file_contents = Vec::new();
|
||||
zip_file.read_to_end(&mut file_contents)?;
|
||||
let stop = f(Entry::Zip(ZipEntry {
|
||||
path: path.into(),
|
||||
is_dir,
|
||||
file_contents,
|
||||
}))
|
||||
.map_err(Into::into)?;
|
||||
if stop {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract an entire source archive into a specified path. If the source is a single compressed
|
||||
/// file and not an archive, it will be extracted into a file with the same name inside of
|
||||
/// `into_dir`.
|
||||
#[allow(dead_code)]
|
||||
pub fn extract_into(&mut self, into_dir: &path::Path) -> Result<()> {
|
||||
match self.archive_format {
|
||||
ArchiveFormat::Tar(_) => {
|
||||
let mut archive = tar::Archive::new(&mut self.reader);
|
||||
archive.unpack(into_dir)?;
|
||||
}
|
||||
|
||||
ArchiveFormat::Zip => {
|
||||
#[cfg(feature = "fs-extract-api")]
|
||||
{
|
||||
let mut archive = zip::ZipArchive::new(self.reader.get_mut())?;
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i)?;
|
||||
// Decode the file name from raw bytes instead of using file.name() directly.
|
||||
// file.name() uses String::from_utf8_lossy() which may return messy characters
|
||||
// such as: 爱交易.app/, that does not work as expected.
|
||||
// Here we require the file name must be a valid UTF-8.
|
||||
let file_name = String::from_utf8(file.name_raw().to_vec())?;
|
||||
let out_path = into_dir.join(file_name);
|
||||
if file.is_dir() {
|
||||
fs::create_dir_all(&out_path)?;
|
||||
} else {
|
||||
if let Some(out_path_parent) = out_path.parent() {
|
||||
fs::create_dir_all(out_path_parent)?;
|
||||
}
|
||||
let mut out_file = fs::File::create(&out_path)?;
|
||||
io::copy(&mut file, &mut out_file)?;
|
||||
}
|
||||
// Get and Set permissions
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if let Some(mode) = file.unix_mode() {
|
||||
fs::set_permissions(&out_path, fs::Permissions::from_mode(mode))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn set_perms(
|
||||
dst: &Path,
|
||||
f: Option<&mut std::fs::File>,
|
||||
mode: u32,
|
||||
preserve: bool,
|
||||
) -> io::Result<()> {
|
||||
_set_perms(dst, f, mode, preserve).map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!(
|
||||
"failed to set permissions to {mode:o} \
|
||||
for `{}`",
|
||||
dst.display()
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn _set_perms(
|
||||
dst: &Path,
|
||||
f: Option<&mut std::fs::File>,
|
||||
mode: u32,
|
||||
preserve: bool,
|
||||
) -> io::Result<()> {
|
||||
use std::os::unix::prelude::*;
|
||||
|
||||
let mode = if preserve { mode } else { mode & 0o777 };
|
||||
let perm = fs::Permissions::from_mode(mode as _);
|
||||
match f {
|
||||
Some(f) => f.set_permissions(perm),
|
||||
None => fs::set_permissions(dst, perm),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn _set_perms(
|
||||
dst: &Path,
|
||||
f: Option<&mut std::fs::File>,
|
||||
mode: u32,
|
||||
_preserve: bool,
|
||||
) -> io::Result<()> {
|
||||
if mode & 0o200 == 0o200 {
|
||||
return Ok(());
|
||||
}
|
||||
match f {
|
||||
Some(f) => {
|
||||
let mut perm = f.metadata()?.permissions();
|
||||
perm.set_readonly(true);
|
||||
f.set_permissions(perm)
|
||||
}
|
||||
None => {
|
||||
let mut perm = fs::metadata(dst)?.permissions();
|
||||
perm.set_readonly(true);
|
||||
fs::set_permissions(dst, perm)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@
|
||||
//! ```
|
||||
|
||||
mod core;
|
||||
mod extract;
|
||||
mod move_file;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -23,7 +25,7 @@ pub use self::core::{DownloadEvent, RemoteRelease};
|
||||
|
||||
use tauri::{AppHandle, Manager, Runtime};
|
||||
|
||||
use crate::Result;
|
||||
use crate::{Result, UpdaterState};
|
||||
|
||||
/// Gets the target string used on the updater.
|
||||
pub fn target() -> Option<String> {
|
||||
@@ -276,7 +278,7 @@ impl<R: Runtime> UpdateResponse<R> {
|
||||
// Linux we replace the AppImage by launching a new install, it start a new AppImage instance, so we're closing the previous. (the process stop here)
|
||||
self.update
|
||||
.download_and_install(
|
||||
self.update.app.config().tauri.updater.pubkey.clone(),
|
||||
self.update.app.config().tauri.bundle.updater.pubkey.clone(),
|
||||
on_event,
|
||||
)
|
||||
.await
|
||||
@@ -285,14 +287,13 @@ impl<R: Runtime> UpdateResponse<R> {
|
||||
|
||||
/// Initializes the [`UpdateBuilder`] using the app configuration.
|
||||
pub fn builder<R: Runtime>(handle: AppHandle<R>) -> UpdateBuilder<R> {
|
||||
let updater_config = &handle.config().tauri.updater;
|
||||
let package_info = handle.package_info().clone();
|
||||
|
||||
// prepare our endpoints
|
||||
let endpoints = updater_config
|
||||
let endpoints = handle
|
||||
.state::<UpdaterState>()
|
||||
.config
|
||||
.endpoints
|
||||
.as_ref()
|
||||
.expect("Something wrong with endpoints")
|
||||
.iter()
|
||||
.map(|e| e.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use ignore::WalkBuilder;
|
||||
use std::{fs, path};
|
||||
|
||||
use crate::Result;
|
||||
|
||||
/// Moves a file from the given path to the specified destination.
|
||||
///
|
||||
/// `source` and `dest` must be on the same filesystem.
|
||||
/// If `replace_using_temp` is specified, the destination file will be
|
||||
/// replaced using the given temporary path.
|
||||
///
|
||||
/// * Errors:
|
||||
/// * Io - copying / renaming
|
||||
#[derive(Debug)]
|
||||
pub struct Move<'a> {
|
||||
source: &'a path::Path,
|
||||
temp: Option<&'a path::Path>,
|
||||
}
|
||||
impl<'a> Move<'a> {
|
||||
/// Specify source file
|
||||
pub fn from_source(source: &'a path::Path) -> Move<'a> {
|
||||
Self { source, temp: None }
|
||||
}
|
||||
|
||||
/// If specified and the destination file already exists, the "destination"
|
||||
/// file will be moved to the given temporary location before the "source"
|
||||
/// file is moved to the "destination" file.
|
||||
///
|
||||
/// In the event of an `io` error while renaming "source" to "destination",
|
||||
/// the temporary file will be moved back to "destination".
|
||||
///
|
||||
/// The `temp` dir must be explicitly provided since `rename` operations require
|
||||
/// files to live on the same filesystem.
|
||||
#[allow(dead_code)]
|
||||
pub fn replace_using_temp(&mut self, temp: &'a path::Path) -> &mut Self {
|
||||
self.temp = Some(temp);
|
||||
self
|
||||
}
|
||||
|
||||
/// Move source file to specified destination (replace whole directory)
|
||||
pub fn to_dest(&self, dest: &path::Path) -> Result<()> {
|
||||
match self.temp {
|
||||
None => {
|
||||
fs::rename(self.source, dest)?;
|
||||
}
|
||||
Some(temp) => {
|
||||
if dest.exists() {
|
||||
fs::rename(dest, temp)?;
|
||||
if let Err(e) = fs::rename(self.source, dest) {
|
||||
fs::rename(temp, dest)?;
|
||||
return Err(e.into());
|
||||
}
|
||||
} else {
|
||||
fs::rename(self.source, dest)?;
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Walk in the source and copy all files and create directories if needed by
|
||||
/// replacing existing elements. (equivalent to a cp -R)
|
||||
#[allow(dead_code)]
|
||||
pub fn walk_to_dest(&self, dest: &path::Path) -> Result<()> {
|
||||
match self.temp {
|
||||
None => {
|
||||
// got no temp -- no need to backup
|
||||
walkdir_and_copy(self.source, dest)?;
|
||||
}
|
||||
Some(temp) => {
|
||||
if dest.exists() {
|
||||
// we got temp and our dest exist, lets make a backup
|
||||
// of current files
|
||||
walkdir_and_copy(dest, temp)?;
|
||||
|
||||
if let Err(e) = walkdir_and_copy(self.source, dest) {
|
||||
// if we got something wrong we reset the dest with our backup
|
||||
fs::rename(temp, dest)?;
|
||||
return Err(e);
|
||||
}
|
||||
} else {
|
||||
// got temp but dest didnt exist
|
||||
walkdir_and_copy(self.source, dest)?;
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
// Walk into the source and create directories, and copy files
|
||||
// Overwriting existing items but keeping untouched the files in the dest
|
||||
// not provided in the source.
|
||||
fn walkdir_and_copy(source: &path::Path, dest: &path::Path) -> Result<()> {
|
||||
let walkdir = WalkBuilder::new(source).hidden(false).build();
|
||||
|
||||
for entry in walkdir {
|
||||
// Check if it's a file
|
||||
|
||||
let element = entry?;
|
||||
let metadata = element.metadata()?;
|
||||
let destination = dest.join(element.path().strip_prefix(source)?);
|
||||
|
||||
// we make sure it's a directory and destination doesnt exist
|
||||
if metadata.is_dir() && !&destination.exists() {
|
||||
fs::create_dir_all(&destination)?;
|
||||
}
|
||||
|
||||
// we make sure it's a file
|
||||
if metadata.is_file() {
|
||||
fs::copy(element.path(), destination)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
-3852
File diff suppressed because it is too large
Load Diff
@@ -7,24 +7,29 @@
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
fn main() {
|
||||
#[allow(unused_mut)]
|
||||
let mut context = tauri::generate_context!();
|
||||
|
||||
let mut updater = tauri_plugin_updater::Builder::new();
|
||||
if std::env::var("TARGET").unwrap_or_default() == "nsis" {
|
||||
// /D sets the default installation directory ($INSTDIR),
|
||||
// overriding InstallDir and InstallDirRegKey.
|
||||
// It must be the last parameter used in the command line and must not contain any quotes, even if the path contains spaces.
|
||||
// Only absolute paths are supported.
|
||||
// NOTE: we only need this because this is an integration test and we don't want to install the app in the programs folder
|
||||
context.config_mut().tauri.updater.windows.installer_args = vec![format!(
|
||||
// TODO mutate plugin config
|
||||
updater = updater.installer_args(vec![format!(
|
||||
"/D={}",
|
||||
tauri::utils::platform::current_exe()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.display()
|
||||
)];
|
||||
)]);
|
||||
}
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(updater.build())
|
||||
.setup(|app| {
|
||||
let handle = app.handle();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
|
||||
@@ -21,18 +21,19 @@
|
||||
"wix": {
|
||||
"skipWebviewInstall": true
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK",
|
||||
"windows": {
|
||||
"installMode": "quiet"
|
||||
}
|
||||
}
|
||||
},
|
||||
"allowlist": {
|
||||
"all": false
|
||||
},
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"active": true,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDE5QzMxNjYwNTM5OEUwNTgKUldSWTRKaFRZQmJER1h4d1ZMYVA3dnluSjdpN2RmMldJR09hUFFlZDY0SlFqckkvRUJhZDJVZXAK",
|
||||
"endpoints": ["http://localhost:3007"],
|
||||
"windows": {
|
||||
"installMode": "quiet"
|
||||
}
|
||||
"endpoints": ["http://localhost:3007"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-3363
File diff suppressed because it is too large
Load Diff
@@ -33,9 +33,6 @@
|
||||
"exceptionDomain": ""
|
||||
}
|
||||
},
|
||||
"allowlist": {
|
||||
"all": false
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"title": "Tauri App",
|
||||
|
||||
Reference in New Issue
Block a user