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