Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/security-scoped-ios.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"fs": patch
"fs-js": patch
---

Enable access for security-scoped resources on iOS by automatically calling `NSURL::startAccessingSecurityScopedResource` on resource access and adding the `stopAccessingSecurityScopedResource` API.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions plugins/fs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,8 @@ notify-debouncer-full = { version = "0.6", optional = true }
dunce = { workspace = true }
percent-encoding = "2"

[target.'cfg(target_os = "ios")'.dependencies]
objc2-foundation = { version = "0.3", features = ["NSURL", "NSString"] }

[features]
watch = ["notify", "notify-debouncer-full"]
2 changes: 1 addition & 1 deletion plugins/fs/api-iife.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions plugins/fs/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const COMMANDS: &[(&str, &[&str])] = &[
// TODO: Remove this in v3
("unwatch", &[]),
("size", &[]),
("stop_accessing_security_scoped_resource", &[]),
];

fn main() {
Expand Down
35 changes: 34 additions & 1 deletion plugins/fs/guest-js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1348,6 +1348,38 @@ async function size(path: string | URL): Promise<number> {
})
}

/**
* Stops accessing a security-scoped resource for the given file URL.
* This should be called when you're done accessing a file that was opened
* using a security-scoped URL (e.g., from a file picker).
*
* #### Platform-specific
*
* - **iOS:** Stops accessing the security-scoped resource.
* - **Other platforms:** No-op (does nothing).
*
* @example
* ```typescript
* import { stopAccessingSecurityScopedResource } from '@tauri-apps/plugin-fs';
*
* // After you're done with a file from a file picker
* await stopAccessingSecurityScopedResource('file:///path/to/file.txt');
* ```
*
* @since 2.4.4
*/
async function stopAccessingSecurityScopedResource(
path: string | URL
): Promise<void> {
if (path instanceof URL && path.protocol !== 'file:') {
throw new TypeError('Must be a file URL.')
}

await invoke('plugin:fs|stop_accessing_security_scoped_resource', {
path: path instanceof URL ? path.toString() : path
})
}

export type {
CreateOptions,
OpenOptions,
Expand Down Expand Up @@ -1396,5 +1428,6 @@ export {
exists,
watch,
watchImmediate,
size
size,
stopAccessingSecurityScopedResource
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!

"$schema" = "../../schemas/schema.json"

[[permission]]
identifier = "allow-stop-accessing-security-scoped-resource"
description = "Enables the stop_accessing_security_scoped_resource command without any pre-configured scope."
commands.allow = ["stop_accessing_security_scoped_resource"]

[[permission]]
identifier = "deny-stop-accessing-security-scoped-resource"
description = "Denies the stop_accessing_security_scoped_resource command without any pre-configured scope."
commands.deny = ["stop_accessing_security_scoped_resource"]
26 changes: 26 additions & 0 deletions plugins/fs/permissions/autogenerated/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -3461,6 +3461,32 @@ Denies the stat command without any pre-configured scope.
<tr>
<td>

`fs:allow-stop-accessing-security-scoped-resource`

</td>
<td>

Enables the stop_accessing_security_scoped_resource command without any pre-configured scope.

</td>
</tr>

<tr>
<td>

`fs:deny-stop-accessing-security-scoped-resource`

</td>
<td>

Denies the stop_accessing_security_scoped_resource command without any pre-configured scope.

</td>
</tr>

<tr>
<td>

`fs:allow-truncate`

</td>
Expand Down
12 changes: 12 additions & 0 deletions plugins/fs/permissions/schemas/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1872,6 +1872,18 @@
"const": "deny-stat",
"markdownDescription": "Denies the stat command without any pre-configured scope."
},
{
"description": "Enables the stop_accessing_security_scoped_resource command without any pre-configured scope.",
"type": "string",
"const": "allow-stop-accessing-security-scoped-resource",
"markdownDescription": "Enables the stop_accessing_security_scoped_resource command without any pre-configured scope."
},
{
"description": "Denies the stop_accessing_security_scoped_resource command without any pre-configured scope.",
"type": "string",
"const": "deny-stop-accessing-security-scoped-resource",
"markdownDescription": "Denies the stop_accessing_security_scoped_resource command without any pre-configured scope."
},
{
"description": "Enables the truncate command without any pre-configured scope.",
"type": "string",
Expand Down
48 changes: 16 additions & 32 deletions plugins/fs/src/mobile.rs → plugins/fs/src/android.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,24 @@
// SPDX-License-Identifier: MIT

use serde::de::DeserializeOwned;
use tauri::{
plugin::{PluginApi, PluginHandle},
AppHandle, Runtime,
};
use tauri::{plugin::PluginApi, AppHandle, Runtime};

use crate::{models::*, FilePath, OpenOptions};

#[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "com.plugin.fs";

#[cfg(target_os = "ios")]
tauri::ios_plugin_binding!(init_plugin_fs);
pub struct Fs<R: Runtime>(tauri::plugin::PluginHandle<R>);

// initializes the Kotlin or Swift plugin classes
pub fn init<R: Runtime, C: DeserializeOwned>(
_app: &AppHandle<R>,
api: PluginApi<R, C>,
) -> crate::Result<Fs<R>> {
#[cfg(target_os = "android")]
let handle = api
.register_android_plugin(PLUGIN_IDENTIFIER, "FsPlugin")
.unwrap();
#[cfg(target_os = "ios")]
let handle = api.register_ios_plugin(init_plugin_android - intent - send)?;
Ok(Fs(handle))
}

/// Access to the android-intent-send APIs.
pub struct Fs<R: Runtime>(PluginHandle<R>);

impl<R: Runtime> Fs<R> {
pub fn open<P: Into<FilePath>>(
&self,
Expand Down Expand Up @@ -68,29 +56,25 @@ impl<R: Runtime> Fs<R> {
}
}

#[cfg(target_os = "android")]
fn resolve_content_uri(
&self,
uri: impl Into<String>,
mode: impl Into<String>,
) -> crate::Result<std::fs::File> {
#[cfg(target_os = "android")]
{
let result = self.0.run_mobile_plugin::<GetFileDescriptorResponse>(
"getFileDescriptor",
GetFileDescriptorPayload {
uri: uri.into(),
mode: mode.into(),
},
)?;
if let Some(fd) = result.fd {
Ok(unsafe {
use std::os::fd::FromRawFd;
std::fs::File::from_raw_fd(fd)
})
} else {
unimplemented!()
}
let result = self.0.run_mobile_plugin::<GetFileDescriptorResponse>(
"getFileDescriptor",
GetFileDescriptorPayload {
uri: uri.into(),
mode: mode.into(),
},
)?;
if let Some(fd) = result.fd {
Ok(unsafe {
use std::os::fd::FromRawFd;
std::fs::File::from_raw_fd(fd)
})
} else {
unimplemented!()
}
}
}
52 changes: 50 additions & 2 deletions plugins/fs/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1010,7 +1010,34 @@ fn get_dir_size(path: &PathBuf) -> CommandResult<u64> {
Ok(size)
}

#[cfg(not(target_os = "android"))]
#[tauri::command]
pub fn stop_accessing_security_scoped_resource<R: Runtime>(
webview: Webview<R>,
path: SafeFilePath,
) -> CommandResult<()> {
#[cfg(target_os = "ios")]
{
use crate::{FilePath, FsExt};
// Convert SafeFilePath to FilePath
let file_path: FilePath = match path {
SafeFilePath::Url(url) => FilePath::Url(url),
SafeFilePath::Path(safe_path) => FilePath::Path(safe_path.as_ref().to_owned()),
};
webview
.fs()
.stop_accessing_security_scoped_resource(file_path)?;
Ok(())
}
#[cfg(not(target_os = "ios"))]
{
// No-op on non-iOS platforms
let _ = webview;
let _ = path;
Ok(())
}
}

#[cfg(desktop)]
pub fn resolve_file<R: Runtime>(
permission: &str,
webview: &Webview<R>,
Expand Down Expand Up @@ -1057,7 +1084,7 @@ fn resolve_file_in_fs<R: Runtime>(
Ok((file, path))
}

#[cfg(target_os = "android")]
#[cfg(mobile)]
pub fn resolve_file<R: Runtime>(
permission: &str,
webview: &Webview<R>,
Expand Down Expand Up @@ -1095,6 +1122,27 @@ pub fn resolve_path<R: Runtime>(
path: SafeFilePath,
base_dir: Option<BaseDirectory>,
) -> CommandResult<PathBuf> {
// On iOS, start accessing security-scoped resource if the path is a file URL
#[cfg(target_os = "ios")]
{
if let SafeFilePath::Url(url) = &path {
if url.scheme() == "file" {
use objc2_foundation::{NSString, NSURL};

let url_string = url.as_str();
let url_nsstring = NSString::from_str(url_string);
let ns_url = unsafe { NSURL::URLWithString(&url_nsstring) };
if let Some(ns_url) = ns_url {
// Start accessing the security-scoped resource
// This is required for files outside the app's sandbox (e.g., from file picker)
unsafe {
let _ = ns_url.startAccessingSecurityScopedResource();
Comment thread
FabianLars marked this conversation as resolved.
Outdated
}
}
}
}
}

let path = path.into_path()?;
let path = if let Some(base_dir) = base_dir {
webview.path().resolve(&path, base_dir)?
Expand Down
Loading
Loading