tauri_plugin_advanced_file_manager/lib.rs
1// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! Access the file system.
6
7#![doc(
8 html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png",
9 html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/app-icon.png"
10)]
11
12use std::io::Read;
13
14use serde::Deserialize;
15use tauri::{
16 ipc::ScopeObject,
17 plugin::{Builder as PluginBuilder, TauriPlugin},
18 utils::{acl::Value, config::FsScope},
19 AppHandle, DragDropEvent, Manager, RunEvent, Runtime, WindowEvent,
20};
21
22mod commands;
23mod config;
24#[cfg(not(target_os = "android"))]
25mod desktop;
26mod error;
27mod file_path;
28#[cfg(target_os = "android")]
29mod mobile;
30#[cfg(target_os = "android")]
31mod models;
32mod scope;
33#[cfg(feature = "watch")]
34mod watcher;
35
36// Dialog 模块 - 完整整合
37pub mod dialog;
38pub use dialog::{
39 Dialog, DialogExt, FileDialogBuilder, MessageDialogBuilder,
40 MessageDialogButtons, MessageDialogKind, MessageDialogResult,
41 PickerMode,
42};
43
44// Opener 模块 - 完整整合
45pub mod opener;
46pub use opener::{Opener, OpenerExt, open_path, open_url, reveal_item_in_dir, reveal_items_in_dir};
47
48#[cfg(not(target_os = "android"))]
49pub use desktop::Fs;
50#[cfg(target_os = "android")]
51pub use mobile::Fs;
52
53pub use error::Error;
54
55pub use file_path::FilePath;
56pub use file_path::SafeFilePath;
57
58type Result<T> = std::result::Result<T, Error>;
59
60#[derive(Debug, Default, Clone, Deserialize)]
61#[serde(rename_all = "camelCase")]
62pub struct OpenOptions {
63 #[serde(default = "default_true")]
64 read: bool,
65 #[serde(default)]
66 write: bool,
67 #[serde(default)]
68 append: bool,
69 #[serde(default)]
70 truncate: bool,
71 #[serde(default)]
72 create: bool,
73 #[serde(default)]
74 create_new: bool,
75 #[serde(default)]
76 #[allow(unused)]
77 mode: Option<u32>,
78 #[serde(default)]
79 #[allow(unused)]
80 custom_flags: Option<i32>,
81}
82
83fn default_true() -> bool {
84 true
85}
86
87impl From<OpenOptions> for std::fs::OpenOptions {
88 fn from(open_options: OpenOptions) -> Self {
89 let mut opts = std::fs::OpenOptions::new();
90
91 #[cfg(unix)]
92 {
93 use std::os::unix::fs::OpenOptionsExt;
94 if let Some(mode) = open_options.mode {
95 opts.mode(mode);
96 }
97 if let Some(flags) = open_options.custom_flags {
98 opts.custom_flags(flags);
99 }
100 }
101
102 opts.read(open_options.read)
103 .write(open_options.write)
104 .create(open_options.create)
105 .append(open_options.append)
106 .truncate(open_options.truncate)
107 .create_new(open_options.create_new);
108
109 opts
110 }
111}
112
113impl OpenOptions {
114 /// Creates a blank new set of options ready for configuration.
115 ///
116 /// All options are initially set to `false`.
117 ///
118 /// # Examples
119 ///
120 /// ```no_run
121 /// use tauri_plugin_advanced_file_manager::OpenOptions;
122 ///
123 /// let mut options = OpenOptions::new();
124 /// options.read(true);
125 /// ```
126 #[must_use]
127 pub fn new() -> Self {
128 Self::default()
129 }
130
131 /// Sets the option for read access.
132 ///
133 /// This option, when true, will indicate that the file should be
134 /// `read`-able if opened.
135 ///
136 /// # Examples
137 ///
138 /// ```no_run
139 /// use tauri_plugin_advanced_file_manager::OpenOptions;
140 ///
141 /// let options = OpenOptions::new().read(true);
142 /// ```
143 pub fn read(&mut self, read: bool) -> &mut Self {
144 self.read = read;
145 self
146 }
147
148 /// Sets the option for write access.
149 ///
150 /// This option, when true, will indicate that the file should be
151 /// `write`-able if opened.
152 ///
153 /// If the file already exists, any write calls on it will overwrite its
154 /// contents, without truncating it.
155 ///
156 /// # Examples
157 ///
158 /// ```no_run
159 /// use tauri_plugin_advanced_file_manager::OpenOptions;
160 ///
161 /// let options = OpenOptions::new().write(true);
162 /// ```
163 pub fn write(&mut self, write: bool) -> &mut Self {
164 self.write = write;
165 self
166 }
167
168 /// Sets the option for the append mode.
169 ///
170 /// This option, when true, means that writes will append to a file instead
171 /// of overwriting previous contents.
172 /// Note that setting `.write(true).append(true)` has the same effect as
173 /// setting only `.append(true)`.
174 ///
175 /// Append mode guarantees that writes will be positioned at the current end of file,
176 /// even when there are other processes or threads appending to the same file. This is
177 /// unlike <code>[seek]\([SeekFrom]::[End]\(0))</code> followed by `write()`, which
178 /// has a race between seeking and writing during which another writer can write, with
179 /// our `write()` overwriting their data.
180 ///
181 /// Keep in mind that this does not necessarily guarantee that data appended by
182 /// different processes or threads does not interleave. The amount of data accepted a
183 /// single `write()` call depends on the operating system and file system. A
184 /// successful `write()` is allowed to write only part of the given data, so even if
185 /// you're careful to provide the whole message in a single call to `write()`, there
186 /// is no guarantee that it will be written out in full. If you rely on the filesystem
187 /// accepting the message in a single write, make sure that all data that belongs
188 /// together is written in one operation. This can be done by concatenating strings
189 /// before passing them to [`write()`].
190 ///
191 /// If a file is opened with both read and append access, beware that after
192 /// opening, and after every write, the position for reading may be set at the
193 /// end of the file. So, before writing, save the current position (using
194 /// <code>[Seek]::[stream_position]</code>), and restore it before the next read.
195 ///
196 /// ## Note
197 ///
198 /// This function doesn't create the file if it doesn't exist. Use the
199 /// [`OpenOptions::create`] method to do so.
200 ///
201 /// [`write()`]: Write::write "io::Write::write"
202 /// [`flush()`]: Write::flush "io::Write::flush"
203 /// [stream_position]: Seek::stream_position "io::Seek::stream_position"
204 /// [seek]: Seek::seek "io::Seek::seek"
205 /// [Current]: SeekFrom::Current "io::SeekFrom::Current"
206 /// [End]: SeekFrom::End "io::SeekFrom::End"
207 ///
208 /// # Examples
209 ///
210 /// ```no_run
211 /// use tauri_plugin_advanced_file_manager::OpenOptions;
212 ///
213 /// let options = OpenOptions::new().append(true);
214 /// ```
215 pub fn append(&mut self, append: bool) -> &mut Self {
216 self.append = append;
217 self
218 }
219
220 /// Sets the option for truncating a previous file.
221 ///
222 /// If a file is successfully opened with this option set it will truncate
223 /// the file to 0 length if it already exists.
224 ///
225 /// The file must be opened with write access for truncate to work.
226 ///
227 /// # Examples
228 ///
229 /// ```no_run
230 /// use tauri_plugin_advanced_file_manager::OpenOptions;
231 ///
232 /// let options = OpenOptions::new().write(true).truncate(true);
233 /// ```
234 pub fn truncate(&mut self, truncate: bool) -> &mut Self {
235 self.truncate = truncate;
236 self
237 }
238
239 /// Sets the option to create a new file, or open it if it already exists.
240 ///
241 /// In order for the file to be created, [`OpenOptions::write`] or
242 /// [`OpenOptions::append`] access must be used.
243 ///
244 ///
245 /// # Examples
246 ///
247 /// ```no_run
248 /// use tauri_plugin_advanced_file_manager::OpenOptions;
249 ///
250 /// let options = OpenOptions::new().write(true).create(true);
251 /// ```
252 pub fn create(&mut self, create: bool) -> &mut Self {
253 self.create = create;
254 self
255 }
256
257 /// Sets the option to create a new file, failing if it already exists.
258 ///
259 /// No file is allowed to exist at the target location, also no (dangling) symlink. In this
260 /// way, if the call succeeds, the file returned is guaranteed to be new.
261 /// If a file exists at the target location, creating a new file will fail with [`AlreadyExists`]
262 /// or another error based on the situation. See [`OpenOptions::open`] for a
263 /// non-exhaustive list of likely errors.
264 ///
265 /// This option is useful because it is atomic. Otherwise between checking
266 /// whether a file exists and creating a new one, the file may have been
267 /// created by another process (a TOCTOU race condition / attack).
268 ///
269 /// If `.create_new(true)` is set, [`.create()`] and [`.truncate()`] are
270 /// ignored.
271 ///
272 /// The file must be opened with write or append access in order to create
273 /// a new file.
274 ///
275 /// [`.create()`]: OpenOptions::create
276 /// [`.truncate()`]: OpenOptions::truncate
277 /// [`AlreadyExists`]: io::ErrorKind::AlreadyExists
278 ///
279 /// # Examples
280 ///
281 /// ```no_run
282 /// use tauri_plugin_advanced_file_manager::OpenOptions;
283 ///
284 /// let options = OpenOptions::new().write(true)
285 /// .create_new(true);
286 /// ```
287 pub fn create_new(&mut self, create_new: bool) -> &mut Self {
288 self.create_new = create_new;
289 self
290 }
291}
292
293#[cfg(unix)]
294impl std::os::unix::fs::OpenOptionsExt for OpenOptions {
295 fn custom_flags(&mut self, flags: i32) -> &mut Self {
296 self.custom_flags.replace(flags);
297 self
298 }
299
300 fn mode(&mut self, mode: u32) -> &mut Self {
301 self.mode.replace(mode);
302 self
303 }
304}
305
306impl OpenOptions {
307 #[cfg(target_os = "android")]
308 fn android_mode(&self) -> String {
309 let mut mode = String::new();
310
311 if self.read {
312 mode.push('r');
313 }
314 if self.write {
315 mode.push('w');
316 }
317 if self.truncate {
318 mode.push('t');
319 }
320 if self.append {
321 mode.push('a');
322 }
323
324 mode
325 }
326}
327
328impl<R: Runtime> Fs<R> {
329 pub fn read_to_string<P: Into<FilePath>>(&self, path: P) -> std::io::Result<String> {
330 let mut s = String::new();
331 self.open(
332 path,
333 OpenOptions {
334 read: true,
335 ..Default::default()
336 },
337 )?
338 .read_to_string(&mut s)?;
339 Ok(s)
340 }
341
342 pub fn read<P: Into<FilePath>>(&self, path: P) -> std::io::Result<Vec<u8>> {
343 let mut buf = Vec::new();
344 self.open(
345 path,
346 OpenOptions {
347 read: true,
348 ..Default::default()
349 },
350 )?
351 .read_to_end(&mut buf)?;
352 Ok(buf)
353 }
354}
355
356// implement ScopeObject here instead of in the scope module because it is also used on the build script
357// and we don't want to add tauri as a build dependency
358impl ScopeObject for scope::Entry {
359 type Error = Error;
360 fn deserialize<R: Runtime>(
361 app: &AppHandle<R>,
362 raw: Value,
363 ) -> std::result::Result<Self, Self::Error> {
364 let path = serde_json::from_value(raw.into()).map(|raw| match raw {
365 scope::EntryRaw::Value(path) => path,
366 scope::EntryRaw::Object { path } => path,
367 })?;
368
369 match app.path().parse(path) {
370 Ok(path) => Ok(Self { path: Some(path) }),
371 #[cfg(not(target_os = "android"))]
372 Err(tauri::Error::UnknownPath) => Ok(Self { path: None }),
373 Err(err) => Err(err.into()),
374 }
375 }
376}
377
378pub(crate) struct Scope {
379 pub(crate) scope: tauri::fs::Scope,
380 pub(crate) require_literal_leading_dot: Option<bool>,
381}
382
383pub trait FsExt<R: Runtime> {
384 fn fs_scope(&self) -> tauri::fs::Scope;
385 fn try_fs_scope(&self) -> Option<tauri::fs::Scope>;
386
387 /// Cross platform file system APIs that also support manipulating Android files.
388 fn fs(&self) -> &Fs<R>;
389}
390
391impl<R: Runtime, T: Manager<R>> FsExt<R> for T {
392 fn fs_scope(&self) -> tauri::fs::Scope {
393 self.state::<Scope>().scope.clone()
394 }
395
396 fn try_fs_scope(&self) -> Option<tauri::fs::Scope> {
397 self.try_state::<Scope>().map(|s| s.scope.clone())
398 }
399
400 fn fs(&self) -> &Fs<R> {
401 self.state::<Fs<R>>().inner()
402 }
403}
404
405pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
406 PluginBuilder::<R, Option<config::Config>>::new("advanced-file-manager")
407 .invoke_handler(tauri::generate_handler![
408 // FS commands
409 commands::create,
410 commands::open,
411 commands::copy_file,
412 commands::mkdir,
413 commands::read_dir,
414 commands::read,
415 commands::read_file,
416 commands::read_text_file,
417 commands::read_text_file_lines,
418 commands::read_text_file_lines_next,
419 commands::remove,
420 commands::rename,
421 commands::seek,
422 commands::stat,
423 commands::lstat,
424 commands::fstat,
425 commands::truncate,
426 commands::ftruncate,
427 commands::write,
428 commands::write_file,
429 commands::write_text_file,
430 commands::exists,
431 commands::size,
432 #[cfg(feature = "watch")]
433 watcher::watch,
434 // Dialog commands
435 dialog::commands::dialog_open,
436 dialog::commands::save,
437 dialog::commands::message,
438 dialog::commands::ask,
439 dialog::commands::confirm,
440 // Opener commands
441 opener::commands::open_url,
442 opener::commands::open_path,
443 opener::commands::reveal_item_in_dir,
444 ])
445 .setup(|app, api| {
446 // 提前获取配置值
447 let require_literal_leading_dot = api
448 .config()
449 .as_ref()
450 .and_then(|c| c.require_literal_leading_dot);
451
452 // FS scope setup
453 let scope = Scope {
454 require_literal_leading_dot,
455 scope: tauri::fs::Scope::new(app, &FsScope::default())?,
456 };
457
458 #[cfg(target_os = "android")]
459 {
460 let fs = mobile::init(app, api)?;
461 app.manage(fs);
462 }
463 #[cfg(not(target_os = "android"))]
464 {
465 app.manage(Fs(app.clone()));
466
467 // Dialog setup (desktop)
468 let dialog = dialog::desktop::init(app, api)?;
469 app.manage(dialog);
470
471 // Opener setup (desktop)
472 let opener: Opener<R> = Opener {
473 _marker: std::marker::PhantomData,
474 require_literal_leading_dot,
475 };
476 app.manage(opener);
477 }
478
479 app.manage(scope);
480
481 Ok(())
482 })
483 .on_event(|app, event| {
484 if let RunEvent::WindowEvent {
485 label: _,
486 event: WindowEvent::DragDrop(DragDropEvent::Drop { paths, position: _ }),
487 ..
488 } = event
489 {
490 let scope = app.fs_scope();
491 for path in paths {
492 if path.is_file() {
493 let _ = scope.allow_file(path);
494 } else {
495 let _ = scope.allow_directory(path, true);
496 }
497 }
498 }
499 })
500 .build()
501}