snapshot of https://github.com/tauri-apps/tauri.git (dev, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Audit / audit-js (push) Failing after 0s
bench / bench (map[os:ubuntu-latest runner:xvfb-run --auto-servernum target:x86_64-unknown-linux-gnu], nightly) (push) Failing after 0s
covector version or publish / run-integration-tests (ubuntu-latest) (push) Failing after 0s
Audit / audit-rust (push) Failing after 0s
test `@tauri-apps/cli` / test (ubuntu-latest) (push) Failing after 0s
test `tauri-cli` / test (map[os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
deploy schema worker / deploy (push) Failing after 0s
lint rust / clippy (map[cargo:cargo include: os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
lint rust / clippy (map[cargo:cross include:--package api os:ubuntu-latest target:aarch64-linux-android]) (push) Failing after 0s
supply chain health status / check rust dependencies with cargo vet (push) Failing after 0s
test `@tauri-apps/api` e2e (mobile) / android (push) Failing after 0s
test `@tauri-apps/api` e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
Udeps / changes (push) Failing after 0s
Udeps / setup (push) Has been skipped
Udeps / udeps (push) Has been skipped
bench / bench (map[os:macos-latest runner: target:aarch64-apple-darwin], nightly) (push) Has been cancelled
bench / bench (map[os:windows-latest runner: target:x86_64-pc-windows-msvc], nightly) (push) Has been cancelled
covector version or publish / run-integration-tests (macos-latest) (push) Has been cancelled
covector version or publish / run-integration-tests (windows-latest) (push) Has been cancelled
covector version or publish / version-or-publish (push) Has been cancelled
test `@tauri-apps/cli` / test (macos-latest) (push) Has been cancelled
test `tauri-cli` / test (map[args:--no-default-features --features native-tls-vendored os:windows-latest restore-only:true target:aarch64-pc-windows-msvc]) (push) Has been cancelled
test `tauri-cli` / test (map[os:macos-latest target:x86_64-apple-darwin]) (push) Has been cancelled
test `tauri-cli` / test (map[os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:macos-latest target:aarch64-apple-darwin]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include:--package api os:macos-latest target:aarch64-apple-ios]) (push) Has been cancelled
test `@tauri-apps/api` e2e (mobile) / ios (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
test `@tauri-apps/cli` / test (windows-latest) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
Some checks failed
Audit / audit-js (push) Failing after 0s
bench / bench (map[os:ubuntu-latest runner:xvfb-run --auto-servernum target:x86_64-unknown-linux-gnu], nightly) (push) Failing after 0s
covector version or publish / run-integration-tests (ubuntu-latest) (push) Failing after 0s
Audit / audit-rust (push) Failing after 0s
test `@tauri-apps/cli` / test (ubuntu-latest) (push) Failing after 0s
test `tauri-cli` / test (map[os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
deploy schema worker / deploy (push) Failing after 0s
lint rust / clippy (map[cargo:cargo include: os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
lint rust / clippy (map[cargo:cross include:--package api os:ubuntu-latest target:aarch64-linux-android]) (push) Failing after 0s
supply chain health status / check rust dependencies with cargo vet (push) Failing after 0s
test `@tauri-apps/api` e2e (mobile) / android (push) Failing after 0s
test `@tauri-apps/api` e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
Udeps / changes (push) Failing after 0s
Udeps / setup (push) Has been skipped
Udeps / udeps (push) Has been skipped
bench / bench (map[os:macos-latest runner: target:aarch64-apple-darwin], nightly) (push) Has been cancelled
bench / bench (map[os:windows-latest runner: target:x86_64-pc-windows-msvc], nightly) (push) Has been cancelled
covector version or publish / run-integration-tests (macos-latest) (push) Has been cancelled
covector version or publish / run-integration-tests (windows-latest) (push) Has been cancelled
covector version or publish / version-or-publish (push) Has been cancelled
test `@tauri-apps/cli` / test (macos-latest) (push) Has been cancelled
test `tauri-cli` / test (map[args:--no-default-features --features native-tls-vendored os:windows-latest restore-only:true target:aarch64-pc-windows-msvc]) (push) Has been cancelled
test `tauri-cli` / test (map[os:macos-latest target:x86_64-apple-darwin]) (push) Has been cancelled
test `tauri-cli` / test (map[os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:macos-latest target:aarch64-apple-darwin]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include:--package api os:macos-latest target:aarch64-apple-ios]) (push) Has been cancelled
test `@tauri-apps/api` e2e (mobile) / ios (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
test `@tauri-apps/cli` / test (windows-latest) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
This commit is contained in:
187
crates/tauri/src/path/android.rs
Normal file
187
crates/tauri/src/path/android.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::Result;
|
||||
use crate::{Runtime, plugin::PluginHandle};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// A helper class to access the mobile path APIs.
|
||||
pub struct PathResolver<R: Runtime>(pub(crate) PluginHandle<R>);
|
||||
|
||||
impl<R: Runtime> Clone for PathResolver<R> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct PathResponse {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct GetFileNameFromUriRequest<'a> {
|
||||
uri: &'a str,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct GetFileNameFromUriResponse {
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
impl<R: Runtime> PathResolver<R> {
|
||||
/// Returns the final component of the `Path`, if there is one.
|
||||
///
|
||||
/// If the path is a normal file, this is the file name. If it's the path of a directory, this
|
||||
/// is the directory name.
|
||||
///
|
||||
/// Returns [`None`] if the path terminates in `..`.
|
||||
///
|
||||
/// On Android this also supports checking the file name of content URIs, such as the values returned by the dialog plugin.
|
||||
///
|
||||
/// If you are dealing with plain file system paths or not worried about Android content URIs, prefer [`Path::file_name`].
|
||||
pub fn file_name(&self, path: &str) -> Option<String> {
|
||||
if path.starts_with("content://") || path.starts_with("file://") {
|
||||
self
|
||||
.0
|
||||
.run_mobile_plugin::<GetFileNameFromUriResponse>(
|
||||
"getFileNameFromUri",
|
||||
GetFileNameFromUriRequest { uri: path },
|
||||
)
|
||||
.map(|r| r.name)
|
||||
.unwrap_or_else(|e| {
|
||||
log::error!("failed to get file name from URI: {e}");
|
||||
None
|
||||
})
|
||||
} else {
|
||||
Path::new(path)
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
fn call_resolve(&self, dir: &str) -> Result<PathBuf> {
|
||||
self
|
||||
.0
|
||||
.run_mobile_plugin::<PathResponse>(dir, ())
|
||||
.map(|r| r.path)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the path to the user's audio directory.
|
||||
pub fn audio_dir(&self) -> Result<PathBuf> {
|
||||
self.call_resolve("getAudioDir")
|
||||
}
|
||||
|
||||
/// Returns the path to the user's cache directory.
|
||||
pub fn cache_dir(&self) -> Result<PathBuf> {
|
||||
self.call_resolve("getExternalCacheDir")
|
||||
}
|
||||
|
||||
/// Returns the path to the user's config directory.
|
||||
pub fn config_dir(&self) -> Result<PathBuf> {
|
||||
self.call_resolve("getConfigDir")
|
||||
}
|
||||
|
||||
/// Returns the path to the user's data directory.
|
||||
pub fn data_dir(&self) -> Result<PathBuf> {
|
||||
self.call_resolve("getDataDir")
|
||||
}
|
||||
|
||||
/// Returns the path to the user's local data directory.
|
||||
pub fn local_data_dir(&self) -> Result<PathBuf> {
|
||||
self.call_resolve("getDataDir")
|
||||
}
|
||||
|
||||
/// Returns the path to the user's document directory.
|
||||
pub fn document_dir(&self) -> Result<PathBuf> {
|
||||
self.call_resolve("getDocumentDir")
|
||||
}
|
||||
|
||||
/// Returns the path to the user's download directory.
|
||||
pub fn download_dir(&self) -> Result<PathBuf> {
|
||||
self.call_resolve("getDownloadDir")
|
||||
}
|
||||
|
||||
/// Returns the path to the user's picture directory.
|
||||
pub fn picture_dir(&self) -> Result<PathBuf> {
|
||||
self.call_resolve("getPictureDir")
|
||||
}
|
||||
|
||||
/// Returns the path to the user's public directory.
|
||||
pub fn public_dir(&self) -> Result<PathBuf> {
|
||||
self.call_resolve("getPublicDir")
|
||||
}
|
||||
|
||||
/// Returns the path to the user's video directory.
|
||||
///
|
||||
/// Resolves to the app-specific Movies directory (`getExternalFilesDir(DIRECTORY_MOVIES)`),
|
||||
/// typically `.../files/Movies`.
|
||||
///
|
||||
/// ## Migration
|
||||
///
|
||||
/// Previously this resolved to external cache storage ([`cache_dir`], typically `.../cache`).
|
||||
/// Files written to the old location will not be discovered at the new path and must be migrated
|
||||
/// or paths updated accordingly.
|
||||
pub fn video_dir(&self) -> Result<PathBuf> {
|
||||
self.call_resolve("getVideoDir")
|
||||
}
|
||||
|
||||
/// Returns the path to the resource directory of this app.
|
||||
pub fn resource_dir(&self) -> Result<PathBuf> {
|
||||
self.call_resolve("getResourcesDir")
|
||||
}
|
||||
|
||||
/// Returns the path to the suggested directory for your app's config files.
|
||||
///
|
||||
/// Resolves to [`config_dir`]`/${bundle_identifier}`.
|
||||
pub fn app_config_dir(&self) -> Result<PathBuf> {
|
||||
self.call_resolve("getConfigDir")
|
||||
}
|
||||
|
||||
/// Returns the path to the suggested directory for your app's data files.
|
||||
///
|
||||
/// Resolves to [`data_dir`]`/${bundle_identifier}`.
|
||||
pub fn app_data_dir(&self) -> Result<PathBuf> {
|
||||
self.call_resolve("getDataDir")
|
||||
}
|
||||
|
||||
/// Returns the path to the suggested directory for your app's local data files.
|
||||
///
|
||||
/// Resolves to [`local_data_dir`]`/${bundle_identifier}`.
|
||||
pub fn app_local_data_dir(&self) -> Result<PathBuf> {
|
||||
self.call_resolve("getDataDir")
|
||||
}
|
||||
|
||||
/// Returns the path to the suggested directory for your app's cache files.
|
||||
///
|
||||
/// Resolves to [`cache_dir`]`/${bundle_identifier}`.
|
||||
pub fn app_cache_dir(&self) -> Result<PathBuf> {
|
||||
self.call_resolve("getCacheDir")
|
||||
}
|
||||
|
||||
/// Returns the path to the suggested directory for your app's log files.
|
||||
pub fn app_log_dir(&self) -> Result<PathBuf> {
|
||||
self
|
||||
.call_resolve("getConfigDir")
|
||||
.map(|dir| dir.join("logs"))
|
||||
}
|
||||
|
||||
/// A temporary directory. Resolves to [`std::env::temp_dir`].
|
||||
pub fn temp_dir(&self) -> Result<PathBuf> {
|
||||
Ok(std::env::temp_dir())
|
||||
}
|
||||
|
||||
/// Returns the path to the user's home directory.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Resolves to `$HOME`.
|
||||
/// - **macOS:** Resolves to `$HOME`.
|
||||
/// - **Windows:** Resolves to `{FOLDERID_Profile}`.
|
||||
/// - **iOS**: Cannot be written to directly, use one of the app paths instead.
|
||||
pub fn home_dir(&self) -> Result<PathBuf> {
|
||||
self.call_resolve("getHomeDir")
|
||||
}
|
||||
}
|
||||
296
crates/tauri/src/path/desktop.rs
Normal file
296
crates/tauri/src/path/desktop.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::{Error, Result};
|
||||
use crate::{AppHandle, Manager, Runtime};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// The path resolver is a helper class for general and application-specific path APIs.
|
||||
pub struct PathResolver<R: Runtime>(pub(crate) AppHandle<R>);
|
||||
|
||||
impl<R: Runtime> Clone for PathResolver<R> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> PathResolver<R> {
|
||||
/// Returns the final component of the `Path`, if there is one.
|
||||
///
|
||||
/// If the path is a normal file, this is the file name. If it's the path of a directory, this
|
||||
/// is the directory name.
|
||||
///
|
||||
/// Returns [`None`] if the path terminates in `..`.
|
||||
///
|
||||
/// On Android this also supports checking the file name of content URIs, such as the values returned by the dialog plugin.
|
||||
///
|
||||
/// If you are dealing with plain file system paths or not worried about Android content URIs, prefer [`Path::file_name`].
|
||||
pub fn file_name(&self, path: &str) -> Option<String> {
|
||||
Path::new(path)
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
/// Returns the path to the user's audio directory.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_MUSIC_DIR`.
|
||||
/// - **macOS:** Resolves to `$HOME/Music`.
|
||||
/// - **Windows:** Resolves to `{FOLDERID_Music}`.
|
||||
pub fn audio_dir(&self) -> Result<PathBuf> {
|
||||
dirs::audio_dir().ok_or(Error::UnknownPath)
|
||||
}
|
||||
|
||||
/// Returns the path to the user's cache directory.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Resolves to `$XDG_CACHE_HOME` or `$HOME/.cache`.
|
||||
/// - **macOS:** Resolves to `$HOME/Library/Caches`.
|
||||
/// - **Windows:** Resolves to `{FOLDERID_LocalAppData}`.
|
||||
pub fn cache_dir(&self) -> Result<PathBuf> {
|
||||
dirs::cache_dir().ok_or(Error::UnknownPath)
|
||||
}
|
||||
|
||||
/// Returns the path to the user's config directory.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Resolves to `$XDG_CONFIG_HOME` or `$HOME/.config`.
|
||||
/// - **macOS:** Resolves to `$HOME/Library/Application Support`.
|
||||
/// - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`.
|
||||
pub fn config_dir(&self) -> Result<PathBuf> {
|
||||
dirs::config_dir().ok_or(Error::UnknownPath)
|
||||
}
|
||||
|
||||
/// Returns the path to the user's data directory.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`.
|
||||
/// - **macOS:** Resolves to `$HOME/Library/Application Support`.
|
||||
/// - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`.
|
||||
pub fn data_dir(&self) -> Result<PathBuf> {
|
||||
dirs::data_dir().ok_or(Error::UnknownPath)
|
||||
}
|
||||
|
||||
/// Returns the path to the user's local data directory.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`.
|
||||
/// - **macOS:** Resolves to `$HOME/Library/Application Support`.
|
||||
/// - **Windows:** Resolves to `{FOLDERID_LocalAppData}`.
|
||||
pub fn local_data_dir(&self) -> Result<PathBuf> {
|
||||
dirs::data_local_dir().ok_or(Error::UnknownPath)
|
||||
}
|
||||
|
||||
/// Returns the path to the user's desktop directory.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DESKTOP_DIR`.
|
||||
/// - **macOS:** Resolves to `$HOME/Desktop`.
|
||||
/// - **Windows:** Resolves to `{FOLDERID_Desktop}`.
|
||||
pub fn desktop_dir(&self) -> Result<PathBuf> {
|
||||
dirs::desktop_dir().ok_or(Error::UnknownPath)
|
||||
}
|
||||
|
||||
/// Returns the path to the user's document directory.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOCUMENTS_DIR`.
|
||||
/// - **macOS:** Resolves to `$HOME/Documents`.
|
||||
/// - **Windows:** Resolves to `{FOLDERID_Documents}`.
|
||||
pub fn document_dir(&self) -> Result<PathBuf> {
|
||||
dirs::document_dir().ok_or(Error::UnknownPath)
|
||||
}
|
||||
|
||||
/// Returns the path to the user's download directory.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_DOWNLOAD_DIR`.
|
||||
/// - **macOS:** Resolves to `$HOME/Downloads`.
|
||||
/// - **Windows:** Resolves to `{FOLDERID_Downloads}`.
|
||||
pub fn download_dir(&self) -> Result<PathBuf> {
|
||||
dirs::download_dir().ok_or(Error::UnknownPath)
|
||||
}
|
||||
|
||||
/// Returns the path to the user's executable directory.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Resolves to `$XDG_BIN_HOME/../bin` or `$XDG_DATA_HOME/../bin` or `$HOME/.local/bin`.
|
||||
/// - **macOS:** Not supported.
|
||||
/// - **Windows:** Not supported.
|
||||
pub fn executable_dir(&self) -> Result<PathBuf> {
|
||||
dirs::executable_dir().ok_or(Error::UnknownPath)
|
||||
}
|
||||
|
||||
/// Returns the path to the user's font directory.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Resolves to `$XDG_DATA_HOME/fonts` or `$HOME/.local/share/fonts`.
|
||||
/// - **macOS:** Resolves to `$HOME/Library/Fonts`.
|
||||
/// - **Windows:** Not supported.
|
||||
pub fn font_dir(&self) -> Result<PathBuf> {
|
||||
dirs::font_dir().ok_or(Error::UnknownPath)
|
||||
}
|
||||
|
||||
/// Returns the path to the user's home directory.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Resolves to `$HOME`.
|
||||
/// - **macOS:** Resolves to `$HOME`.
|
||||
/// - **Windows:** Resolves to `{FOLDERID_Profile}`.
|
||||
/// - **iOS**: Cannot be written to directly, use one of the app paths instead.
|
||||
pub fn home_dir(&self) -> Result<PathBuf> {
|
||||
dirs::home_dir().ok_or(Error::UnknownPath)
|
||||
}
|
||||
|
||||
/// Returns the path to the user's picture directory.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PICTURES_DIR`.
|
||||
/// - **macOS:** Resolves to `$HOME/Pictures`.
|
||||
/// - **Windows:** Resolves to `{FOLDERID_Pictures}`.
|
||||
pub fn picture_dir(&self) -> Result<PathBuf> {
|
||||
dirs::picture_dir().ok_or(Error::UnknownPath)
|
||||
}
|
||||
|
||||
/// Returns the path to the user's public directory.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_PUBLICSHARE_DIR`.
|
||||
/// - **macOS:** Resolves to `$HOME/Public`.
|
||||
/// - **Windows:** Resolves to `{FOLDERID_Public}`.
|
||||
pub fn public_dir(&self) -> Result<PathBuf> {
|
||||
dirs::public_dir().ok_or(Error::UnknownPath)
|
||||
}
|
||||
|
||||
/// Returns the path to the user's runtime directory.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Resolves to `$XDG_RUNTIME_DIR`.
|
||||
/// - **macOS:** Not supported.
|
||||
/// - **Windows:** Not supported.
|
||||
pub fn runtime_dir(&self) -> Result<PathBuf> {
|
||||
dirs::runtime_dir().ok_or(Error::UnknownPath)
|
||||
}
|
||||
|
||||
/// Returns the path to the user's template directory.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_TEMPLATES_DIR`.
|
||||
/// - **macOS:** Not supported.
|
||||
/// - **Windows:** Resolves to `{FOLDERID_Templates}`.
|
||||
pub fn template_dir(&self) -> Result<PathBuf> {
|
||||
dirs::template_dir().ok_or(Error::UnknownPath)
|
||||
}
|
||||
|
||||
/// Returns the path to the user's video dir
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Resolves to [`xdg-user-dirs`](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/)' `XDG_VIDEOS_DIR`.
|
||||
/// - **macOS:** Resolves to `$HOME/Movies`.
|
||||
/// - **Windows:** Resolves to `{FOLDERID_Videos}`.
|
||||
pub fn video_dir(&self) -> Result<PathBuf> {
|
||||
dirs::video_dir().ok_or(Error::UnknownPath)
|
||||
}
|
||||
|
||||
/// Returns the path to the resource directory of this app.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// Although we provide the exact path where this function resolves to,
|
||||
/// this is not a contract and things might change in the future
|
||||
///
|
||||
/// - **Windows:** Resolves to the directory that contains the main executable.
|
||||
/// - **Linux:** When running in an AppImage, the `APPDIR` variable will be set to
|
||||
/// the mounted location of the app, and the resource dir will be `${APPDIR}/usr/lib/${exe_name}`.
|
||||
/// If not running in an AppImage, the path is `/usr/lib/${exe_name}`.
|
||||
/// When running the app from `src-tauri/target/(debug|release)/`, the path is `${exe_dir}/../lib/${exe_name}`.
|
||||
/// - **macOS:** Resolves to `${exe_dir}/../Resources` (inside .app).
|
||||
/// - **iOS:** Resolves to `${exe_dir}/assets`.
|
||||
/// - **Android:** Currently the resources are stored in the APK as assets so it's not a normal file system path,
|
||||
/// we return a special URI prefix `asset://localhost/` here that can be used with the [file system plugin](https://tauri.app/plugin/file-system/),
|
||||
/// with that, you can read the files through [`FsExt::fs`](https://docs.rs/tauri-plugin-fs/latest/tauri_plugin_fs/trait.FsExt.html#tymethod.fs)
|
||||
/// like this: `app.fs().read_to_string(app.path().resource_dir().unwrap().join("resource"));`
|
||||
pub fn resource_dir(&self) -> Result<PathBuf> {
|
||||
crate::utils::platform::resource_dir(self.0.package_info(), &self.0.env())
|
||||
.map_err(|_| Error::UnknownPath)
|
||||
}
|
||||
|
||||
/// Returns the path to the suggested directory for your app's config files.
|
||||
///
|
||||
/// Resolves to [`config_dir`](Self::config_dir)`/${bundle_identifier}`.
|
||||
pub fn app_config_dir(&self) -> Result<PathBuf> {
|
||||
dirs::config_dir()
|
||||
.ok_or(Error::UnknownPath)
|
||||
.map(|dir| dir.join(&self.0.config().identifier))
|
||||
}
|
||||
|
||||
/// Returns the path to the suggested directory for your app's data files.
|
||||
///
|
||||
/// Resolves to [`data_dir`](Self::data_dir)`/${bundle_identifier}`.
|
||||
pub fn app_data_dir(&self) -> Result<PathBuf> {
|
||||
dirs::data_dir()
|
||||
.ok_or(Error::UnknownPath)
|
||||
.map(|dir| dir.join(&self.0.config().identifier))
|
||||
}
|
||||
|
||||
/// Returns the path to the suggested directory for your app's local data files.
|
||||
///
|
||||
/// Resolves to [`local_data_dir`](Self::local_data_dir)`/${bundle_identifier}`.
|
||||
pub fn app_local_data_dir(&self) -> Result<PathBuf> {
|
||||
dirs::data_local_dir()
|
||||
.ok_or(Error::UnknownPath)
|
||||
.map(|dir| dir.join(&self.0.config().identifier))
|
||||
}
|
||||
|
||||
/// Returns the path to the suggested directory for your app's cache files.
|
||||
///
|
||||
/// Resolves to [`cache_dir`](Self::cache_dir)`/${bundle_identifier}`.
|
||||
pub fn app_cache_dir(&self) -> Result<PathBuf> {
|
||||
dirs::cache_dir()
|
||||
.ok_or(Error::UnknownPath)
|
||||
.map(|dir| dir.join(&self.0.config().identifier))
|
||||
}
|
||||
|
||||
/// Returns the path to the suggested directory for your app's log files.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Linux:** Resolves to [`local_data_dir`](Self::local_data_dir)`/${bundle_identifier}/logs`.
|
||||
/// - **macOS:** Resolves to [`home_dir`](Self::home_dir)`/Library/Logs/${bundle_identifier}`
|
||||
/// - **Windows:** Resolves to [`local_data_dir`](Self::local_data_dir)`/${bundle_identifier}/logs`.
|
||||
pub fn app_log_dir(&self) -> Result<PathBuf> {
|
||||
#[cfg(target_os = "macos")]
|
||||
let path = dirs::home_dir()
|
||||
.ok_or(Error::UnknownPath)
|
||||
.map(|dir| dir.join("Library/Logs").join(&self.0.config().identifier));
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let path = dirs::data_local_dir()
|
||||
.ok_or(Error::UnknownPath)
|
||||
.map(|dir| dir.join(&self.0.config().identifier).join("logs"));
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
/// A temporary directory. Resolves to [`std::env::temp_dir`].
|
||||
pub fn temp_dir(&self) -> Result<PathBuf> {
|
||||
Ok(std::env::temp_dir())
|
||||
}
|
||||
}
|
||||
10
crates/tauri/src/path/init.js
Normal file
10
crates/tauri/src/path/init.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
Object.defineProperty(window.__TAURI_INTERNALS__.plugins, 'path', {
|
||||
value: {
|
||||
sep: __TEMPLATE_sep__,
|
||||
delimiter: __TEMPLATE_delimiter__
|
||||
}
|
||||
})
|
||||
386
crates/tauri/src/path/mod.rs
Normal file
386
crates/tauri/src/path/mod.rs
Normal file
@@ -0,0 +1,386 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
path::{Component, Display, Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use crate::Runtime;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
|
||||
pub(crate) mod plugin;
|
||||
|
||||
use crate::error::*;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
mod android;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
mod desktop;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub use android::PathResolver;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub use desktop::PathResolver;
|
||||
|
||||
/// A wrapper for [`PathBuf`] that prevents path traversal.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tauri::path::SafePathBuf;
|
||||
/// assert!(SafePathBuf::new("../secret.txt".into()).is_err());
|
||||
/// assert!(SafePathBuf::new("/home/user/stuff/../secret.txt".into()).is_err());
|
||||
///
|
||||
/// assert!(SafePathBuf::new("./file.txt".into()).is_ok());
|
||||
/// assert!(SafePathBuf::new("/home/user/secret.txt".into()).is_ok());
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct SafePathBuf(PathBuf);
|
||||
|
||||
impl SafePathBuf {
|
||||
/// Validates the path for directory traversal vulnerabilities and returns a new [`SafePathBuf`] instance if it is safe.
|
||||
pub fn new(path: PathBuf) -> std::result::Result<Self, &'static str> {
|
||||
if path.components().any(|x| matches!(x, Component::ParentDir)) {
|
||||
return Err("cannot traverse directory, rewrite the path without the use of `../`");
|
||||
}
|
||||
Ok(Self(path))
|
||||
}
|
||||
|
||||
/// Returns an object that implements [`std::fmt::Display`] for safely printing paths.
|
||||
///
|
||||
/// See [`PathBuf#method.display`] for more information.
|
||||
pub fn display(&self) -> Display<'_> {
|
||||
self.0.display()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for SafePathBuf {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SafePathBuf {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
Self::new(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SafePathBuf> for PathBuf {
|
||||
fn from(path: SafePathBuf) -> Self {
|
||||
path.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SafePathBuf {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let path = PathBuf::deserialize(deserializer)?;
|
||||
SafePathBuf::new(path).map_err(DeError::custom)
|
||||
}
|
||||
}
|
||||
|
||||
/// A base directory for a path.
|
||||
///
|
||||
/// The base directory is the optional root of a file system operation.
|
||||
/// If informed by the API call, all paths will be relative to the path of the given directory.
|
||||
///
|
||||
/// For more information, check the [`dirs` documentation](https://docs.rs/dirs/).
|
||||
#[derive(Serialize_repr, Deserialize_repr, Clone, Copy, Debug)]
|
||||
#[repr(u16)]
|
||||
#[non_exhaustive]
|
||||
pub enum BaseDirectory {
|
||||
/// The Audio directory.
|
||||
/// Resolves to [`crate::path::PathResolver::audio_dir`].
|
||||
Audio = 1,
|
||||
/// The Cache directory.
|
||||
/// Resolves to [`crate::path::PathResolver::cache_dir`].
|
||||
Cache = 2,
|
||||
/// The Config directory.
|
||||
/// Resolves to [`crate::path::PathResolver::config_dir`].
|
||||
Config = 3,
|
||||
/// The Data directory.
|
||||
/// Resolves to [`crate::path::PathResolver::data_dir`].
|
||||
Data = 4,
|
||||
/// The LocalData directory.
|
||||
/// Resolves to [`crate::path::PathResolver::local_data_dir`].
|
||||
LocalData = 5,
|
||||
/// The Document directory.
|
||||
/// Resolves to [`crate::path::PathResolver::document_dir`].
|
||||
Document = 6,
|
||||
/// The Download directory.
|
||||
/// Resolves to [`crate::path::PathResolver::download_dir`].
|
||||
Download = 7,
|
||||
/// The Picture directory.
|
||||
/// Resolves to [`crate::path::PathResolver::picture_dir`].
|
||||
Picture = 8,
|
||||
/// The Public directory.
|
||||
/// Resolves to [`crate::path::PathResolver::public_dir`].
|
||||
Public = 9,
|
||||
/// The Video directory.
|
||||
/// Resolves to [`crate::path::PathResolver::video_dir`].
|
||||
Video = 10,
|
||||
/// The Resource directory.
|
||||
/// Resolves to [`crate::path::PathResolver::resource_dir`].
|
||||
Resource = 11,
|
||||
/// A temporary directory.
|
||||
/// Resolves to [`std::env::temp_dir`].
|
||||
Temp = 12,
|
||||
/// The default app config directory.
|
||||
/// Resolves to [`BaseDirectory::Config`]`/{bundle_identifier}`.
|
||||
AppConfig = 13,
|
||||
/// The default app data directory.
|
||||
/// Resolves to [`BaseDirectory::Data`]`/{bundle_identifier}`.
|
||||
AppData = 14,
|
||||
/// The default app local data directory.
|
||||
/// Resolves to [`BaseDirectory::LocalData`]`/{bundle_identifier}`.
|
||||
AppLocalData = 15,
|
||||
/// The default app cache directory.
|
||||
/// Resolves to [`BaseDirectory::Cache`]`/{bundle_identifier}`.
|
||||
AppCache = 16,
|
||||
/// The default app log directory.
|
||||
/// Resolves to [`BaseDirectory::Home`]`/Library/Logs/{bundle_identifier}` on macOS
|
||||
/// and [`BaseDirectory::Config`]`/{bundle_identifier}/logs` on linux and Windows.
|
||||
AppLog = 17,
|
||||
/// The Desktop directory.
|
||||
/// Resolves to [`crate::path::PathResolver::desktop_dir`].
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Desktop = 18,
|
||||
/// The Executable directory.
|
||||
/// Resolves to [`crate::path::PathResolver::executable_dir`].
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Executable = 19,
|
||||
/// The Font directory.
|
||||
/// Resolves to [`crate::path::PathResolver::font_dir`].
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Font = 20,
|
||||
/// The Home directory.
|
||||
/// Resolves to [`crate::path::PathResolver::home_dir`].
|
||||
Home = 21,
|
||||
/// The Runtime directory.
|
||||
/// Resolves to [`crate::path::PathResolver::runtime_dir`].
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Runtime = 22,
|
||||
/// The Template directory.
|
||||
/// Resolves to [`crate::path::PathResolver::template_dir`].
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Template = 23,
|
||||
}
|
||||
|
||||
impl BaseDirectory {
|
||||
/// Gets the variable that represents this [`BaseDirectory`] for string paths.
|
||||
pub fn variable(self) -> &'static str {
|
||||
match self {
|
||||
Self::Audio => "$AUDIO",
|
||||
Self::Cache => "$CACHE",
|
||||
Self::Config => "$CONFIG",
|
||||
Self::Data => "$DATA",
|
||||
Self::LocalData => "$LOCALDATA",
|
||||
Self::Document => "$DOCUMENT",
|
||||
Self::Download => "$DOWNLOAD",
|
||||
Self::Picture => "$PICTURE",
|
||||
Self::Public => "$PUBLIC",
|
||||
Self::Video => "$VIDEO",
|
||||
Self::Resource => "$RESOURCE",
|
||||
Self::Temp => "$TEMP",
|
||||
Self::AppConfig => "$APPCONFIG",
|
||||
Self::AppData => "$APPDATA",
|
||||
Self::AppLocalData => "$APPLOCALDATA",
|
||||
Self::AppCache => "$APPCACHE",
|
||||
Self::AppLog => "$APPLOG",
|
||||
Self::Home => "$HOME",
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Self::Desktop => "$DESKTOP",
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Self::Executable => "$EXE",
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Self::Font => "$FONT",
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Self::Runtime => "$RUNTIME",
|
||||
#[cfg(not(target_os = "android"))]
|
||||
Self::Template => "$TEMPLATE",
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the [`BaseDirectory`] associated with the given variable, or [`None`] if the variable doesn't match any.
|
||||
pub fn from_variable(variable: &str) -> Option<Self> {
|
||||
let res = match variable {
|
||||
"$AUDIO" => Self::Audio,
|
||||
"$CACHE" => Self::Cache,
|
||||
"$CONFIG" => Self::Config,
|
||||
"$DATA" => Self::Data,
|
||||
"$LOCALDATA" => Self::LocalData,
|
||||
"$DOCUMENT" => Self::Document,
|
||||
"$DOWNLOAD" => Self::Download,
|
||||
|
||||
"$PICTURE" => Self::Picture,
|
||||
"$PUBLIC" => Self::Public,
|
||||
"$VIDEO" => Self::Video,
|
||||
"$RESOURCE" => Self::Resource,
|
||||
"$TEMP" => Self::Temp,
|
||||
"$APPCONFIG" => Self::AppConfig,
|
||||
"$APPDATA" => Self::AppData,
|
||||
"$APPLOCALDATA" => Self::AppLocalData,
|
||||
"$APPCACHE" => Self::AppCache,
|
||||
"$APPLOG" => Self::AppLog,
|
||||
"$HOME" => Self::Home,
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
"$DESKTOP" => Self::Desktop,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
"$EXE" => Self::Executable,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
"$FONT" => Self::Font,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
"$RUNTIME" => Self::Runtime,
|
||||
#[cfg(not(target_os = "android"))]
|
||||
"$TEMPLATE" => Self::Template,
|
||||
|
||||
_ => return None,
|
||||
};
|
||||
Some(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Runtime> PathResolver<R> {
|
||||
/// Resolves the path with the base directory.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::{path::BaseDirectory, Manager};
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(|app| {
|
||||
/// let path = app.path().resolve("path/to/something", BaseDirectory::Config)?;
|
||||
/// assert_eq!(path.to_str().unwrap(), "/home/${whoami}/.config/path/to/something");
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
pub fn resolve<P: AsRef<Path>>(&self, path: P, base_directory: BaseDirectory) -> Result<PathBuf> {
|
||||
resolve_path::<R>(self, base_directory, Some(path.as_ref().to_path_buf()))
|
||||
}
|
||||
|
||||
/// Parse the given path, resolving a [`BaseDirectory`] variable if the path starts with one.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use tauri::Manager;
|
||||
/// tauri::Builder::default()
|
||||
/// .setup(|app| {
|
||||
/// let path = app.path().parse("$HOME/.bashrc")?;
|
||||
/// assert_eq!(path.to_str().unwrap(), "/home/${whoami}/.bashrc");
|
||||
/// Ok(())
|
||||
/// });
|
||||
/// ```
|
||||
pub fn parse<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
|
||||
let mut p = PathBuf::new();
|
||||
let mut components = path.as_ref().components();
|
||||
match components.next() {
|
||||
Some(Component::Normal(str)) => {
|
||||
if let Some(base_directory) = BaseDirectory::from_variable(&str.to_string_lossy()) {
|
||||
p.push(resolve_path::<R>(self, base_directory, None)?);
|
||||
} else {
|
||||
p.push(str);
|
||||
}
|
||||
}
|
||||
Some(component) => p.push(component),
|
||||
None => (),
|
||||
}
|
||||
|
||||
for component in components {
|
||||
if let Component::ParentDir = component {
|
||||
continue;
|
||||
}
|
||||
p.push(component);
|
||||
}
|
||||
|
||||
Ok(p)
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_path<R: Runtime>(
|
||||
resolver: &PathResolver<R>,
|
||||
directory: BaseDirectory,
|
||||
path: Option<PathBuf>,
|
||||
) -> Result<PathBuf> {
|
||||
let resolve_resource = matches!(directory, BaseDirectory::Resource);
|
||||
let mut base_dir_path = match directory {
|
||||
BaseDirectory::Audio => resolver.audio_dir(),
|
||||
BaseDirectory::Cache => resolver.cache_dir(),
|
||||
BaseDirectory::Config => resolver.config_dir(),
|
||||
BaseDirectory::Data => resolver.data_dir(),
|
||||
BaseDirectory::LocalData => resolver.local_data_dir(),
|
||||
BaseDirectory::Document => resolver.document_dir(),
|
||||
BaseDirectory::Download => resolver.download_dir(),
|
||||
BaseDirectory::Picture => resolver.picture_dir(),
|
||||
BaseDirectory::Public => resolver.public_dir(),
|
||||
BaseDirectory::Video => resolver.video_dir(),
|
||||
BaseDirectory::Resource => resolver.resource_dir(),
|
||||
BaseDirectory::Temp => resolver.temp_dir(),
|
||||
BaseDirectory::AppConfig => resolver.app_config_dir(),
|
||||
BaseDirectory::AppData => resolver.app_data_dir(),
|
||||
BaseDirectory::AppLocalData => resolver.app_local_data_dir(),
|
||||
BaseDirectory::AppCache => resolver.app_cache_dir(),
|
||||
BaseDirectory::AppLog => resolver.app_log_dir(),
|
||||
BaseDirectory::Home => resolver.home_dir(),
|
||||
#[cfg(not(target_os = "android"))]
|
||||
BaseDirectory::Desktop => resolver.desktop_dir(),
|
||||
#[cfg(not(target_os = "android"))]
|
||||
BaseDirectory::Executable => resolver.executable_dir(),
|
||||
#[cfg(not(target_os = "android"))]
|
||||
BaseDirectory::Font => resolver.font_dir(),
|
||||
#[cfg(not(target_os = "android"))]
|
||||
BaseDirectory::Runtime => resolver.runtime_dir(),
|
||||
#[cfg(not(target_os = "android"))]
|
||||
BaseDirectory::Template => resolver.template_dir(),
|
||||
}?;
|
||||
|
||||
if let Some(path) = path {
|
||||
// use the same path resolution mechanism as the bundler's resource injection algorithm
|
||||
if resolve_resource {
|
||||
let mut resource_path = PathBuf::new();
|
||||
for component in path.components() {
|
||||
match component {
|
||||
Component::Prefix(_) => {}
|
||||
Component::RootDir => resource_path.push("_root_"),
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => resource_path.push("_up_"),
|
||||
Component::Normal(p) => resource_path.push(p),
|
||||
}
|
||||
}
|
||||
base_dir_path.push(resource_path);
|
||||
} else {
|
||||
base_dir_path.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(base_dir_path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::SafePathBuf;
|
||||
use quickcheck::{Arbitrary, Gen};
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
impl Arbitrary for SafePathBuf {
|
||||
fn arbitrary(g: &mut Gen) -> Self {
|
||||
Self(PathBuf::arbitrary(g))
|
||||
}
|
||||
|
||||
fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
|
||||
Box::new(self.0.shrink().map(SafePathBuf))
|
||||
}
|
||||
}
|
||||
}
|
||||
315
crates/tauri/src/path/plugin.rs
Normal file
315
crates/tauri/src/path/plugin.rs
Normal file
@@ -0,0 +1,315 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::{Component, MAIN_SEPARATOR, Path, PathBuf};
|
||||
|
||||
use serialize_to_javascript::{DefaultTemplate, Template, default_template};
|
||||
|
||||
use super::{BaseDirectory, Error, PathResolver, Result};
|
||||
use crate::{
|
||||
AppHandle, Manager, Runtime, State, command,
|
||||
plugin::{Builder, TauriPlugin},
|
||||
};
|
||||
|
||||
/// Normalize a path, removing things like `.` and `..`, this snippet is taken from cargo's paths util.
|
||||
/// <https://github.com/rust-lang/cargo/blob/46fa867ff7043e3a0545bf3def7be904e1497afd/crates/cargo-util/src/paths.rs#L73-L106>
|
||||
fn normalize_path(path: &Path) -> PathBuf {
|
||||
let mut components = path.components().peekable();
|
||||
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() {
|
||||
components.next();
|
||||
PathBuf::from(c.as_os_str())
|
||||
} else {
|
||||
PathBuf::new()
|
||||
};
|
||||
|
||||
for component in components {
|
||||
match component {
|
||||
Component::Prefix(..) => unreachable!(),
|
||||
Component::RootDir => {
|
||||
ret.push(component.as_os_str());
|
||||
}
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => {
|
||||
ret.pop();
|
||||
}
|
||||
Component::Normal(c) => {
|
||||
ret.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
/// Normalize a path, removing things like `.` and `..`, this snippet is taken from cargo's paths util but
|
||||
/// slightly modified to not resolve absolute paths.
|
||||
/// <https://github.com/rust-lang/cargo/blob/46fa867ff7043e3a0545bf3def7be904e1497afd/crates/cargo-util/src/paths.rs#L73-L106>
|
||||
fn normalize_path_no_absolute(path: &Path) -> PathBuf {
|
||||
let mut components = path.components().peekable();
|
||||
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().copied() {
|
||||
components.next();
|
||||
PathBuf::from(c.as_os_str())
|
||||
} else {
|
||||
PathBuf::new()
|
||||
};
|
||||
|
||||
for component in components {
|
||||
match component {
|
||||
Component::Prefix(..) => unreachable!(),
|
||||
Component::RootDir => {
|
||||
ret.push(component.as_os_str());
|
||||
}
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => {
|
||||
ret.pop();
|
||||
}
|
||||
Component::Normal(c) => {
|
||||
// Using PathBuf::push here will replace the whole path if an absolute path is encountered
|
||||
// which is not the intended behavior, so instead of that, convert the current resolved path
|
||||
// to a string and do simple string concatenation with the current component then convert it
|
||||
// back to a PathBuf
|
||||
let mut p = ret.to_string_lossy().to_string();
|
||||
// Only add a separator if it doesn't have one already or if current normalized path is empty,
|
||||
// this ensures it won't have an unwanted leading separator
|
||||
if !p.is_empty() && !p.ends_with('/') && !p.ends_with('\\') {
|
||||
p.push(MAIN_SEPARATOR);
|
||||
}
|
||||
if let Some(c) = c.to_str() {
|
||||
p.push_str(c);
|
||||
}
|
||||
ret = PathBuf::from(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub fn resolve_directory<R: Runtime>(
|
||||
_app: AppHandle<R>,
|
||||
resolver: State<'_, PathResolver<R>>,
|
||||
directory: BaseDirectory,
|
||||
path: Option<PathBuf>,
|
||||
) -> Result<PathBuf> {
|
||||
super::resolve_path(&resolver, directory, path).map(|p| dunce::simplified(&p).to_path_buf())
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub fn resolve(paths: Vec<String>) -> Result<PathBuf> {
|
||||
// Start with current directory then start adding paths from the vector one by one using `PathBuf.push()` which
|
||||
// will ensure that if an absolute path is encountered in the iteration, it will be used as the current full path.
|
||||
//
|
||||
// examples:
|
||||
// 1. `vec!["."]` or `vec![]` will be equal to `std::env::current_dir()`
|
||||
// 2. `vec!["/foo/bar", "/tmp/file", "baz"]` will be equal to `PathBuf::from("/tmp/file/baz")`
|
||||
let mut path = std::env::current_dir().map_err(Error::CurrentDir)?;
|
||||
for p in paths {
|
||||
path.push(p);
|
||||
}
|
||||
Ok(dunce::simplified(&normalize_path(&path)).to_path_buf())
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub fn normalize(path: String) -> String {
|
||||
let mut p = dunce::simplified(&normalize_path_no_absolute(Path::new(&path)))
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
// Node.js behavior is to return `".."` for `normalize("..")`
|
||||
// and `"."` for `normalize("")` or `normalize(".")`
|
||||
if p.is_empty() && path == ".." {
|
||||
"..".into()
|
||||
} else if p.is_empty() && (path.is_empty() || path == ".") {
|
||||
".".into()
|
||||
} else {
|
||||
// Add a trailing separator if the path passed to this functions had a trailing separator. That's how Node.js behaves.
|
||||
if (path.ends_with('/') || path.ends_with('\\')) && (!p.ends_with('/') || !p.ends_with('\\')) {
|
||||
p.push(MAIN_SEPARATOR);
|
||||
}
|
||||
p
|
||||
}
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub fn join(paths: Vec<String>) -> String {
|
||||
let path = PathBuf::from(
|
||||
paths
|
||||
.into_iter()
|
||||
.map(|mut p| {
|
||||
// Add a `MAIN_SEPARATOR` if it doesn't already have one and is not an empty string.
|
||||
// Doing this to ensure that the vector elements are separated in
|
||||
// the resulting string so path.components() can work correctly when called
|
||||
// in `normalize_path_no_absolute()` later on.
|
||||
if !p.is_empty() && !p.ends_with('/') && !p.ends_with('\\') {
|
||||
p.push(MAIN_SEPARATOR);
|
||||
}
|
||||
p
|
||||
})
|
||||
.collect::<String>(),
|
||||
);
|
||||
|
||||
let p = dunce::simplified(&normalize_path_no_absolute(&path))
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
if p.is_empty() { ".".into() } else { p }
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub fn dirname(path: String) -> Result<PathBuf> {
|
||||
match Path::new(&path).parent() {
|
||||
Some(p) => Ok(dunce::simplified(p).to_path_buf()),
|
||||
None => Err(Error::NoParent),
|
||||
}
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub fn extname<R: Runtime>(app: AppHandle<R>, path: String) -> Result<String> {
|
||||
let file_name = app.path().file_name(&path).ok_or(Error::NoExtension)?;
|
||||
match Path::new(&file_name)
|
||||
.extension()
|
||||
.and_then(std::ffi::OsStr::to_str)
|
||||
{
|
||||
Some(p) => Ok(p.to_string()),
|
||||
None => Err(Error::NoExtension),
|
||||
}
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub fn basename<R: Runtime>(app: AppHandle<R>, path: &str, ext: Option<&str>) -> Result<String> {
|
||||
let file_name = app.path().file_name(path);
|
||||
match file_name {
|
||||
Some(p) => {
|
||||
let maybe_stripped = if let Some(ext) = ext {
|
||||
p.strip_suffix(ext).unwrap_or(&p).to_string()
|
||||
} else {
|
||||
p
|
||||
};
|
||||
Ok(maybe_stripped)
|
||||
}
|
||||
None => Err(Error::NoBasename),
|
||||
}
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub fn is_absolute(path: String) -> bool {
|
||||
Path::new(&path).is_absolute()
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[default_template("./init.js")]
|
||||
struct InitJavascript {
|
||||
sep: &'static str,
|
||||
delimiter: &'static str,
|
||||
}
|
||||
|
||||
/// Initializes the plugin.
|
||||
pub(crate) fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
#[cfg(windows)]
|
||||
let (sep, delimiter) = ("\\", ";");
|
||||
#[cfg(not(windows))]
|
||||
let (sep, delimiter) = ("/", ":");
|
||||
|
||||
let init_js = InitJavascript { sep, delimiter }
|
||||
.render_default(&Default::default())
|
||||
// this will never fail with the above sep and delimiter values
|
||||
.unwrap();
|
||||
|
||||
Builder::new("path")
|
||||
.invoke_handler(crate::generate_handler![
|
||||
#![plugin(path)]
|
||||
resolve_directory,
|
||||
resolve,
|
||||
normalize,
|
||||
join,
|
||||
dirname,
|
||||
extname,
|
||||
basename,
|
||||
is_absolute
|
||||
])
|
||||
.js_init_script(init_js.to_string())
|
||||
.setup(|app, _api| {
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let handle = _api.register_android_plugin("app.tauri", "PathPlugin")?;
|
||||
app.manage(PathResolver(handle));
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
app.manage(PathResolver(app.clone()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test::mock_app;
|
||||
|
||||
#[test]
|
||||
fn basename() {
|
||||
let app = mock_app();
|
||||
let path = "/path/to/some-json-file.json";
|
||||
assert_eq!(
|
||||
super::basename(app.handle().clone(), path, Some(".json")).unwrap(),
|
||||
"some-json-file"
|
||||
);
|
||||
|
||||
let path = "/path/to/some-json-file.json";
|
||||
assert_eq!(
|
||||
super::basename(app.handle().clone(), path, Some("json")).unwrap(),
|
||||
"some-json-file."
|
||||
);
|
||||
|
||||
let path = "/path/to/some-json-file.html.json";
|
||||
assert_eq!(
|
||||
super::basename(app.handle().clone(), path, Some(".json")).unwrap(),
|
||||
"some-json-file.html"
|
||||
);
|
||||
|
||||
let path = "/path/to/some-json-file.json.json";
|
||||
assert_eq!(
|
||||
super::basename(app.handle().clone(), path, Some(".json")).unwrap(),
|
||||
"some-json-file.json"
|
||||
);
|
||||
|
||||
let path = "/path/to/some-json-file.json.html";
|
||||
assert_eq!(
|
||||
super::basename(app.handle().clone(), path, Some(".json")).unwrap(),
|
||||
"some-json-file.json.html"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join() {
|
||||
fn check(paths: Vec<&str>, expected_unix: &str, expected_windows: &str) {
|
||||
let expected = if cfg!(windows) {
|
||||
expected_windows
|
||||
} else {
|
||||
expected_unix
|
||||
};
|
||||
let paths = paths.into_iter().map(String::from).collect();
|
||||
assert_eq!(super::join(paths), expected);
|
||||
}
|
||||
|
||||
check(vec![""], ".", ".");
|
||||
check(vec!["", ""], ".", ".");
|
||||
check(vec!["a"], "a", "a");
|
||||
check(vec!["", "a"], "a", "a");
|
||||
check(vec!["a", "b"], "a/b", r"a\b");
|
||||
check(vec!["a", "", "b"], "a/b", r"a\b");
|
||||
check(vec!["a", "/b", "c"], "a/b/c", r"a\b\c");
|
||||
check(vec!["a", "b/c", "d"], "a/b/c/d", r"a\b\c\d");
|
||||
check(vec!["a/", "b"], "a/b", r"a\b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize() {
|
||||
assert_eq!(super::normalize("".into()), ".");
|
||||
assert_eq!(super::normalize(".".into()), ".");
|
||||
assert_eq!(super::normalize("..".into()), "..");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user