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}