Skip to main content

tauri_plugin_android_fs/api/models/
fs_uri.rs

1use crate::*;
2use serde::{Deserialize, Serialize};
3
4#[deprecated = "Use `tauri_plugin_android_fs::FsUri` instead."]
5pub type FileUri = FsUri;
6
7/// URI for a file or directory.
8///
9/// # Note
10/// Serialized by `serde` as:
11///
12/// ```ts
13/// type AndroidFsUri = {
14///     uri: string,
15///     documentTopTreeUri: string | null
16/// }
17/// ```
18#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize, Serialize)]
19#[serde(rename_all = "camelCase")]
20pub struct FsUri {
21
22    /// URI for a file or directory.
23    ///
24    /// # Note
25    /// This is a URI with either the `content` or `file` scheme.
26    pub uri: String,
27
28    /// Document tree URI of the root directory from which this entry originates.
29    ///
30    /// # Note
31    /// This field is set for directories obtained via Directory Picker
32    /// and for entries derived from those directories.
33    pub document_top_tree_uri: Option<String>,
34}
35
36impl FsUri {
37
38    /// Same as `serde_json::to_string()`
39    pub fn to_json_string(&self) -> Result<String> {
40        serde_json::to_string(self).map_err(Into::into)
41    }
42
43    /// Same as `serde_json::from_str()`
44    pub fn from_json_str(json: impl AsRef<str>) -> Result<Self> {
45        serde_json::from_str(json.as_ref()).map_err(Into::into)
46    }
47
48    pub fn from_uri(uri: impl Into<String>) -> Self {
49        FsUri {
50            uri: uri.into(),
51            document_top_tree_uri: None,
52        }
53    }
54
55    /// Constructs a URI from the absolute path of a file or directory.
56    ///
57    /// # Note
58    /// The path must be absolute and must not contain `./` or `../`.
59    /// Even if the path is invalid, this function will not return an error or panic;
60    /// instead, it returns an invalid URI.
61    ///
62    /// Note the following:
63    /// - This URI cannot be used with [`Opener`](crate::api::api_async::Opener) to open files in other apps.
64    /// - Operations using this URI may fall back to [`std::fs`] instead of the Kotlin API.
65    pub fn from_path(path: impl AsRef<std::path::Path>) -> Self {
66        Self {
67            uri: path_to_android_file_uri(path),
68            document_top_tree_uri: None,
69        }
70    }
71
72    /// Returns the path if this URI uses the `file` scheme;
73    /// otherwise, returns `None`.
74    pub fn to_path(&self) -> Option<std::path::PathBuf> {
75        if self.is_file_scheme() {
76            return Some(android_file_uri_to_path(&self.uri));
77        }
78        None
79    }
80
81    /// Returns `true` if this URI uses the `file` scheme.
82    pub fn is_file_scheme(&self) -> bool {
83        self.uri.starts_with("file://")
84    }
85
86    /// Returns `true` if this URI uses the `content` scheme.
87    pub fn is_content_scheme(&self) -> bool {
88        self.uri.starts_with("content://")
89    }
90}
91
92impl From<&std::path::Path> for FsUri {
93    fn from(path: &std::path::Path) -> Self {
94        Self::from_path(path)
95    }
96}
97
98impl From<&std::path::PathBuf> for FsUri {
99    fn from(path: &std::path::PathBuf) -> Self {
100        Self::from_path(path)
101    }
102}
103
104impl From<std::path::PathBuf> for FsUri {
105    fn from(path: std::path::PathBuf) -> Self {
106        Self::from_path(path)
107    }
108}
109
110impl From<tauri_plugin_fs::FilePath> for FsUri {
111    fn from(value: tauri_plugin_fs::FilePath) -> Self {
112        match value {
113            tauri_plugin_fs::FilePath::Url(url) => Self::from_uri(url),
114            tauri_plugin_fs::FilePath::Path(path) => Self::from_path(path),
115        }
116    }
117}
118
119impl From<FsUri> for tauri_plugin_fs::FilePath {
120    fn from(value: FsUri) -> Self {
121        type NeverErr<T> = std::result::Result<T, std::convert::Infallible>;
122        NeverErr::unwrap(value.uri.parse())
123    }
124}
125
126fn android_file_uri_to_path(uri: impl AsRef<str>) -> std::path::PathBuf {
127    let uri = uri.as_ref();
128    let path_part = uri.strip_prefix("file://").unwrap_or(uri);
129    let decoded = percent_encoding::percent_decode_str(path_part).decode_utf8_lossy();
130
131    std::path::PathBuf::from(decoded.as_ref())
132}
133
134fn path_to_android_file_uri(path: impl AsRef<std::path::Path>) -> String {
135    let encoded = path
136        .as_ref()
137        .to_string_lossy()
138        .split('/')
139        .map(|s| encode_android_uri_component(s))
140        .collect::<Vec<_>>()
141        .join("/");
142
143    format!("file://{}", encoded)
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use std::path::Path;
150
151    #[test]
152    fn test_android_safe_characters() {
153        let path = Path::new("/sdcard/test_file-name!.~'()*.txt");
154        let uri = path_to_android_file_uri(path);
155
156        assert_eq!(uri, "file:///sdcard/test_file-name!.~'()*.txt");
157        assert_eq!(android_file_uri_to_path(&uri), path);
158    }
159
160    #[test]
161    fn test_spaces_and_unsafe_chars() {
162        let path = Path::new("/sdcard/My Documents/file @#$%.txt");
163        let uri = path_to_android_file_uri(path);
164
165        assert_eq!(uri, "file:///sdcard/My%20Documents/file%20%40%23%24%25.txt");
166        assert_eq!(android_file_uri_to_path(&uri), path);
167    }
168
169    #[test]
170    fn test_unicode_characters() {
171        let path = Path::new("/sdcard/ダウンロード");
172        let uri = path_to_android_file_uri(path);
173
174        assert_eq!(
175            uri,
176            "file:///sdcard/%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89"
177        );
178        assert_eq!(android_file_uri_to_path(&uri), path);
179    }
180}