mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-01-31 00:35:19 +01:00
feat(core): resolve file names from Android content URIs (#13012)
* feat(core): resolve file names from Android content URIs This PR adds a new Android path plugin function to resolve file names from content URIs. `PathResolver::file_name` was added to expose this API on Rust, and the existing `@tauri-apps/api/path` basename and extname function now leverages it on Android. Closes https://github.com/tauri-apps/plugins-workspace/issues/1775 Tauri core port from https://github.com/tauri-apps/plugins-workspace/pull/2421 Co-authored-by: VulnX * update change file [skip ci] Co-authored-by: VulnX <62636727+VulnX@users.noreply.github.com> --------- Co-authored-by: VulnX <62636727+VulnX@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
71cb1e26d7
commit
bcdd510254
6
.changes/path-file-name-android-api.md
Normal file
6
.changes/path-file-name-android-api.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"tauri": minor:feat
|
||||
"@tauri-apps/api": minor:feat
|
||||
---
|
||||
|
||||
The `path` basename and extname APIs now accept Android content URIs, such as the paths returned by the dialog plugin.
|
||||
5
.changes/path-file-name-android.md
Normal file
5
.changes/path-file-name-android.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri": minor:feat
|
||||
---
|
||||
|
||||
Added `PathResolver::file_name` to resolve file names from content URIs on Android (leverating `std::path::Path::file_name` on other platforms).
|
||||
@@ -5,9 +5,13 @@
|
||||
package app.tauri
|
||||
|
||||
import android.app.Activity
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.OpenableColumns
|
||||
import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.InvokeArg
|
||||
import app.tauri.annotation.TauriPlugin
|
||||
import app.tauri.plugin.Plugin
|
||||
import app.tauri.plugin.Invoke
|
||||
@@ -15,6 +19,11 @@ import app.tauri.plugin.JSObject
|
||||
|
||||
const val TAURI_ASSETS_DIRECTORY_URI = "asset://localhost/"
|
||||
|
||||
@InvokeArg
|
||||
class GetFileNameFromUriArgs {
|
||||
lateinit var uri: String
|
||||
}
|
||||
|
||||
@TauriPlugin
|
||||
class PathPlugin(private val activity: Activity): Plugin(activity) {
|
||||
private fun resolvePath(invoke: Invoke, path: String?) {
|
||||
@@ -23,6 +32,15 @@ class PathPlugin(private val activity: Activity): Plugin(activity) {
|
||||
invoke.resolve(obj)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun getFileNameFromUri(invoke: Invoke) {
|
||||
val args = invoke.parseArgs(GetFileNameFromUriArgs::class.java)
|
||||
val name = getRealNameFromURI(activity, Uri.parse(args.uri))
|
||||
val res = JSObject()
|
||||
res.put("name", name)
|
||||
invoke.resolve(res)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun getAudioDir(invoke: Invoke) {
|
||||
resolvePath(invoke, activity.getExternalFilesDir(Environment.DIRECTORY_MUSIC)?.absolutePath)
|
||||
@@ -91,3 +109,24 @@ class PathPlugin(private val activity: Activity): Plugin(activity) {
|
||||
resolvePath(invoke, Environment.getExternalStorageDirectory().absolutePath)
|
||||
}
|
||||
}
|
||||
|
||||
fun getRealNameFromURI(activity: Activity, contentUri: Uri): String? {
|
||||
var cursor: Cursor? = null
|
||||
try {
|
||||
val projection = arrayOf(OpenableColumns.DISPLAY_NAME)
|
||||
cursor = activity.contentResolver.query(contentUri, projection, null, null, null)
|
||||
|
||||
cursor?.let {
|
||||
val columnIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (it.moveToFirst()) {
|
||||
return it.getString(columnIndex)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.error("failed to get real name from URI $e")
|
||||
} finally {
|
||||
cursor?.close()
|
||||
}
|
||||
|
||||
return null // Return null if no file name could be resolved
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
use super::Result;
|
||||
use crate::{plugin::PluginHandle, Runtime};
|
||||
use std::path::PathBuf;
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// A helper class to access the mobile path APIs.
|
||||
pub struct PathResolver<R: Runtime>(pub(crate) PluginHandle<R>);
|
||||
@@ -20,7 +23,47 @@ 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
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
use super::{Error, Result};
|
||||
use crate::{AppHandle, Manager, Runtime};
|
||||
use std::path::PathBuf;
|
||||
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>);
|
||||
@@ -16,6 +16,22 @@ impl<R: Runtime> Clone for PathResolver<R> {
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -169,8 +169,9 @@ pub fn dirname(path: String) -> Result<PathBuf> {
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub fn extname(path: String) -> Result<String> {
|
||||
match Path::new(&path)
|
||||
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)
|
||||
{
|
||||
@@ -180,8 +181,8 @@ pub fn extname(path: String) -> Result<String> {
|
||||
}
|
||||
|
||||
#[command(root = "crate")]
|
||||
pub fn basename(path: &str, ext: Option<&str>) -> Result<String> {
|
||||
let file_name = Path::new(path).file_name().map(|f| f.to_string_lossy());
|
||||
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 {
|
||||
@@ -251,36 +252,38 @@ pub(crate) fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
|
||||
#[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(path, Some(".json")).unwrap(),
|
||||
super::basename(app.handle().clone(), path, Some(".json")).unwrap(),
|
||||
"some-json-file"
|
||||
);
|
||||
|
||||
let path = "/path/to/some-json-file.json";
|
||||
assert_eq!(
|
||||
super::basename(path, Some("json")).unwrap(),
|
||||
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(path, Some(".json")).unwrap(),
|
||||
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(path, Some(".json")).unwrap(),
|
||||
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(path, Some(".json")).unwrap(),
|
||||
super::basename(app.handle().clone(), path, Some(".json")).unwrap(),
|
||||
"some-json-file.json.html"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user