Skip to main content

tauri_plugin_android_fs/models/
file_uri.rs

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