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}