Skip to main content

tauri_plugin_android_fs/models/
file_uri.rs

1use serde::{Deserialize, Serialize};
2use crate::*;
3
4
5/// Path to represent a file or directory.
6/// 
7/// # Note
8/// For compatibility, an interconversion to [`tauri_plugin_fs::FilePath`] is implemented, such as follwing.  
9/// This is lossy and also not guaranteed to work properly with other plugins.  
10/// However, reading and writing files by official [`tauri_plugin_fs`] etc. should work well.  
11/// ```ignore
12/// use tauri_plugin_android_fs::FileUri;
13/// use tauri_plugin_fs::FilePath;
14/// 
15/// let uri: FileUri = unimplemented!();
16/// let path: FilePath = uri.into();
17/// let uri: FileUri = path.into();
18/// ```
19/// 
20/// # Typescript type
21/// ```typescript
22/// type FileUri = {
23///     uri: string, // This can use as path for official tauri_plugin_fs
24///     documentTopTreeUri: string | null
25/// }
26/// ```
27#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize, Serialize)]
28#[serde(rename_all = "camelCase")]
29pub struct FileUri {
30    pub uri: String,
31    pub document_top_tree_uri: Option<String>,
32}
33
34#[allow(unused)]
35impl FileUri {
36
37    /// Same as `serde_json::to_string(...)`
38    pub fn to_json_string(&self) -> Result<String> {
39        serde_json::to_string(self).map_err(Into::into)
40    }
41
42    /// Same as `serde_json::from_str(...)`
43    pub fn from_json_str(json: impl AsRef<str>) -> Result<Self> {
44        serde_json::from_str(json.as_ref()).map_err(Into::into)
45    }
46
47    pub fn from_uri(uri: impl Into<String>) -> Self {
48        FileUri {
49            uri: uri.into(),
50            document_top_tree_uri: None 
51        }
52    }
53
54    /// Constructs a URI from the absolute path of a file or directory.   
55    /// 
56    /// This must be an absolute path that does not contain `./` or `../`.
57    /// Even if the path is invalid, it will not cause an error or panic; an invalid URI will be returned.   
58    /// 
59    /// # Note
60    /// There are a few points to note regarding this.
61    /// - This URI cannot be passed to functions of [`FileOpener`](crate::api::api_async::FileOpener) for sending to other apps.
62    /// - Operations using this URI may fall back to [`std::fs`] instead of Kotlin API.
63    pub fn from_path(path: impl AsRef<std::path::Path>) -> Self {
64        Self {
65            uri: path_to_android_file_uri(path), 
66            document_top_tree_uri: None
67        }
68    }
69
70    /// If this URI is an Android file-scheme URI, for example,
71    /// via [`FileUri::from_path`], its absolute path will be retrieved.
72    pub fn to_path(&self) -> Option<std::path::PathBuf> {
73        if self.is_file_uri() {
74            return Some(android_file_uri_to_path(&self.uri))
75        }
76        None
77    }
78
79    /// Indicates whether this is `file://` URI.
80    pub fn is_file_uri(&self) -> bool {
81        self.uri.starts_with("file://")
82    }
83
84    /// Indicates whether this is `content://` URI.
85    pub fn is_content_uri(&self) -> bool {
86        self.uri.starts_with("content://")
87    }
88
89    pub(crate) fn require_content_uri(&self) -> Result<()> {
90        if self.is_content_uri() {
91            Ok(())
92        }
93        else {
94            Err(Error::invalid_uri_scheme(&self.uri))
95        }
96    }
97}
98
99impl From<&std::path::Path> for FileUri {
100
101    fn from(path: &std::path::Path) -> Self {
102        Self::from_path(path)
103    }
104}
105
106impl From<&std::path::PathBuf> for FileUri {
107
108    fn from(path: &std::path::PathBuf) -> Self {
109        Self::from_path(path)
110    }
111}
112
113impl From<std::path::PathBuf> for FileUri {
114
115    fn from(path: std::path::PathBuf) -> Self {
116        Self::from_path(path)
117    }
118}
119
120impl From<tauri_plugin_fs::FilePath> for FileUri {
121
122    fn from(value: tauri_plugin_fs::FilePath) -> Self {
123        match value {
124            tauri_plugin_fs::FilePath::Url(url) => Self::from_uri(url),
125            tauri_plugin_fs::FilePath::Path(path) => Self::from_path(path),
126        }
127    }
128}
129
130impl From<FileUri> for tauri_plugin_fs::FilePath {
131
132    fn from(value: FileUri) -> Self {
133        type NeverErr<T> = std::result::Result::<T, std::convert::Infallible>;
134        NeverErr::unwrap(value.uri.parse())
135    }
136}
137
138
139fn android_file_uri_to_path(uri: impl AsRef<str>) -> std::path::PathBuf {
140    let uri = uri.as_ref();
141    let path_part = uri.strip_prefix("file://").unwrap_or(uri);
142    let decoded = percent_encoding::percent_decode_str(path_part)
143        .decode_utf8_lossy();
144
145    std::path::PathBuf::from(decoded.as_ref())
146}
147
148fn path_to_android_file_uri(path: impl AsRef<std::path::Path>) -> String {
149    let encoded = path
150        .as_ref()
151        .to_string_lossy()
152        .split('/')
153        .map(|s| encode_android_uri_component(s))
154        .collect::<Vec<_>>()
155        .join("/");
156
157    format!("file://{}", encoded)
158}
159
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use std::path::Path;
165
166    #[test]
167    fn test_android_safe_characters() {
168        let path = Path::new("/sdcard/test_file-name!.~'()*.txt");
169        let uri = path_to_android_file_uri(path);
170        
171        assert_eq!(uri, "file:///sdcard/test_file-name!.~'()*.txt");
172        assert_eq!(android_file_uri_to_path(&uri), path);
173    }
174
175    #[test]
176    fn test_spaces_and_unsafe_chars() {
177        let path = Path::new("/sdcard/My Documents/file @#$%.txt");
178        let uri = path_to_android_file_uri(path);
179        
180        assert_eq!(uri, "file:///sdcard/My%20Documents/file%20%40%23%24%25.txt");
181        assert_eq!(android_file_uri_to_path(&uri), path);
182    }
183
184    #[test]
185    fn test_unicode_characters() {
186        let path = Path::new("/sdcard/ダウンロード");
187        let uri = path_to_android_file_uri(path);
188        
189        assert_eq!(uri, "file:///sdcard/%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89");
190        assert_eq!(android_file_uri_to_path(&uri), path);
191    }
192}