Skip to main content

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