Skip to main content

workflow_store/
fs.rs

1//!
2//! File system abstraction layer. Currently supporting storage on the filesystem
3//! and the browser domain-associated local storage ([Web Storage API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API)).
4//!
5//! Storage APIs abstracted:
6//! - Rust std file I/O (fs::xxx)
7//! - NodeJS file I/O (fs::read_file_sync)
8//! - Browser local storage
9//!
10//! By default, all I/O functions will use the name of the file as a key
11//! for localstorage. If you want to manually specify the localstorage key.
12//!
13
14use crate::error::Error;
15use crate::result::Result;
16use cfg_if::cfg_if;
17use js_sys::Reflect;
18use js_sys::Uint8Array;
19use serde::Serialize;
20use serde::de::DeserializeOwned;
21use std::path::{Path, PathBuf};
22use wasm_bindgen::prelude::*;
23use workflow_core::dirs;
24use workflow_core::runtime;
25
26#[wasm_bindgen]
27extern "C" {
28    /// Binding to the Node.js `Buffer` type, used when exchanging binary
29    /// data with the JavaScript runtime.
30    #[wasm_bindgen(extends = Uint8Array)]
31    #[derive(Clone, Debug)]
32    pub type Buffer;
33
34    /// Creates a `Buffer` that wraps the contents of the given `Uint8Array`.
35    #[wasm_bindgen(static_method_of = Buffer, js_name = from)]
36    pub fn from_uint8_array(array: &Uint8Array) -> Buffer;
37
38}
39
40/// Returns the browser's domain-associated `localStorage` object, panicking
41/// if it is not available in the current environment.
42pub fn local_storage() -> web_sys::Storage {
43    web_sys::window()
44        .unwrap()
45        .local_storage()
46        .ok()
47        .flatten()
48        .expect("localStorage is not available")
49}
50
51/// Per-operation options for the file system abstraction, primarily used to
52/// override the key under which data is stored in browser local storage.
53#[derive(Default)]
54pub struct Options {
55    /// Explicit local storage key to use instead of deriving one from the file name.
56    pub local_storage_key: Option<String>,
57}
58
59impl Options {
60    /// Creates [`Options`] that store data under the given explicit local storage key.
61    pub fn with_local_storage_key(key: &str) -> Self {
62        Options {
63            local_storage_key: Some(key.to_string()),
64        }
65    }
66
67    /// Resolves the local storage key for the given file, using the explicit
68    /// key if set, otherwise falling back to the file's name.
69    pub fn local_storage_key(&self, filename: &Path) -> String {
70        self.local_storage_key
71            .clone()
72            .unwrap_or(filename.file_name().unwrap().to_str().unwrap().to_string())
73    }
74}
75
76cfg_if! {
77    if #[cfg(target_arch = "wasm32")] {
78        use workflow_core::hex::*;
79        use workflow_wasm::jserror::*;
80        use workflow_node as node;
81        use js_sys::Object;
82        use workflow_chrome::storage::LocalStorage as ChromeStorage;
83
84
85        pub async fn exists_with_options<P : AsRef<Path>>(filename: P, options : Options) -> Result<bool> {
86            if runtime::is_node() || runtime::is_nw() {
87                let filename = filename.as_ref().to_platform_string();
88                Ok(node::fs::exists_sync(filename.as_ref())?)
89            } else {
90                let key_name = options.local_storage_key(filename.as_ref());
91                if runtime::is_chrome_extension(){
92                    Ok(ChromeStorage::get_item(&key_name).await?.is_some())
93                }else{
94                    Ok(local_storage().get_item(&key_name)?.is_some())
95                }
96            }
97        }
98
99        pub fn exists_with_options_sync<P : AsRef<Path>>(filename: P, options : Options) -> Result<bool> {
100            if runtime::is_node() || runtime::is_nw() {
101                let filename = filename.as_ref().to_platform_string();
102                Ok(node::fs::exists_sync(filename.as_ref())?)
103            } else {
104                let key_name = options.local_storage_key(filename.as_ref());
105                if runtime::is_chrome_extension(){
106                    Err(Error::Custom("localStorage api is unavailable, you can use exists_with_options() for chrome.storage.local api.".to_string()))
107                }else{
108                    Ok(local_storage().get_item(&key_name)?.is_some())
109                }
110            }
111        }
112
113        pub async fn read_to_string_with_options<P : AsRef<Path>>(filename: P, options : Options) -> Result<String> {
114            if runtime::is_node() || runtime::is_nw() {
115                let filename = filename.as_ref().to_platform_string();
116                let options = Object::new();
117                Reflect::set(&options, &"encoding".into(), &"utf-8".into())?;
118                let js_value = node::fs::read_file_sync(&filename, options)?;
119                let text = js_value.as_string().ok_or(Error::DataIsNotAString(filename))?;
120                Ok(text)
121            } else {
122                let key_name = options.local_storage_key(filename.as_ref());
123                if runtime::is_chrome_extension(){
124                    if let Some(text) = ChromeStorage::get_item(&key_name).await?{
125                        Ok(text)
126                    }else {
127                        Err(Error::NotFound(filename.as_ref().to_string_lossy().to_string()))
128                    }
129                }else if let Some(text) = local_storage().get_item(&key_name)? {
130                    Ok(text)
131                } else {
132                    Err(Error::NotFound(filename.as_ref().to_string_lossy().to_string()))
133                }
134            }
135        }
136
137        pub fn read_to_string_with_options_sync<P : AsRef<Path>>(filename: P, options : Options) -> Result<String> {
138            if runtime::is_node() || runtime::is_nw() {
139                let filename = filename.as_ref().to_platform_string();
140                let options = Object::new();
141                Reflect::set(&options, &"encoding".into(), &"utf-8".into())?;
142                let js_value = node::fs::read_file_sync(&filename, options)?;
143                let text = js_value.as_string().ok_or(Error::DataIsNotAString(filename))?;
144                Ok(text)
145            } else {
146                let key_name = options.local_storage_key(filename.as_ref());
147                if runtime::is_chrome_extension(){
148                    Err(Error::Custom("localStorage api is unavailable, you can use exists_with_options() for chrome.storage.local api.".to_string()))
149                }else if let Some(text) = local_storage().get_item(&key_name)? {
150                    Ok(text)
151                } else {
152                    Err(Error::NotFound(filename.as_ref().to_string_lossy().to_string()))
153                }
154            }
155        }
156
157        pub async fn read_binary_with_options<P : AsRef<Path>>(filename: P, options : Options) -> Result<Vec<u8>> {
158            if runtime::is_node() || runtime::is_nw() {
159                let filename = filename.as_ref().to_platform_string();
160                let options = Object::new();
161                let buffer = node::fs::read_file_sync(&filename, options)?;
162                let data = buffer.dyn_into::<Uint8Array>()?;
163                Ok(data.to_vec())
164            } else {
165                let key_name = options.local_storage_key(filename.as_ref());
166                let data = if runtime::is_chrome_extension(){
167                    ChromeStorage::get_item(&key_name).await?
168                }else{
169                    local_storage().get_item(&key_name)?
170                };
171
172                if let Some(text) = data{
173                    let data = Vec::<u8>::from_hex(&text)?;
174                    Ok(data)
175                } else {
176                    Err(Error::NotFound(filename.as_ref().to_string_lossy().to_string()))
177                }
178            }
179        }
180
181        pub fn read_binary_with_options_sync<P : AsRef<Path>>(filename: P, options : Options) -> Result<Vec<u8>> {
182            if runtime::is_node() || runtime::is_nw() {
183                let filename = filename.as_ref().to_platform_string();
184                let options = Object::new();
185                let buffer = node::fs::read_file_sync(&filename, options)?;
186                let data = buffer.dyn_into::<Uint8Array>()?;
187                Ok(data.to_vec())
188            } else if runtime::is_chrome_extension(){
189                    Err(Error::Custom("localStorage api is unavailable, you can use read_binary_with_options() for chrome.storage.local api.".to_string()))
190            } else {
191                let key_name = options.local_storage_key(filename.as_ref());
192                if let Some(text) = local_storage().get_item(&key_name)? {
193                    let data = Vec::<u8>::from_hex(&text)?;
194                    Ok(data)
195                } else {
196                    Err(Error::NotFound(filename.as_ref().to_string_lossy().to_string()))
197                }
198            }
199        }
200
201        pub async fn write_string_with_options<P : AsRef<Path>>(filename: P, options: Options, text : &str) -> Result<()> {
202            if runtime::is_node() || runtime::is_nw() {
203                let filename = filename.as_ref().to_platform_string();
204                let options = Object::new();
205                Reflect::set(&options, &"encoding".into(), &"utf-8".into())?;
206                let data = JsValue::from(text);
207                node::fs::write_file_sync(&filename, data, options)?;
208            } else {
209                let key_name = options.local_storage_key(filename.as_ref());
210                if runtime::is_chrome_extension(){
211                    ChromeStorage::set_item(&key_name, text).await?;
212                }else{
213                    local_storage().set_item(&key_name, text)?;
214                }
215            }
216
217            Ok(())
218        }
219
220        pub fn write_string_with_options_sync<P : AsRef<Path>>(filename: P, options: Options, text : &str) -> Result<()> {
221            if runtime::is_node() || runtime::is_nw() {
222                let filename = filename.as_ref().to_platform_string();
223                let options = Object::new();
224                Reflect::set(&options, &"encoding".into(), &"utf-8".into())?;
225                let data = JsValue::from(text);
226                node::fs::write_file_sync(&filename, data, options)?;
227            } else if runtime::is_chrome_extension(){
228                return Err(Error::Custom("localStorage api is unavailable, you can use write_string_with_options() for chrome.storage.local api.".to_string()));
229            }else{
230                let key_name = options.local_storage_key(filename.as_ref());
231                local_storage().set_item(&key_name, text)?;
232            }
233            Ok(())
234        }
235
236        pub async fn write_binary_with_options<P : AsRef<Path>>(filename: P, options: Options, data : &[u8]) -> Result<()> {
237            if runtime::is_node() || runtime::is_nw() {
238                let filename = filename.as_ref().to_platform_string();
239                let options = Object::new();
240                let uint8_array = Uint8Array::from(data);
241                let buffer = Buffer::from_uint8_array(&uint8_array);
242                node::fs::write_file_sync(&filename, buffer.into(), options)?;
243            } else {
244                let key_name = options.local_storage_key(filename.as_ref());
245                if runtime::is_chrome_extension(){
246                    ChromeStorage::set_item(&key_name, data.to_hex().as_str()).await?;
247                }else{
248                    local_storage().set_item(&key_name, data.to_hex().as_str())?;
249                }
250            }
251            Ok(())
252        }
253
254        pub fn write_binary_with_options_sync<P : AsRef<Path>>(filename: P, options: Options, data : &[u8]) -> Result<()> {
255            if runtime::is_node() || runtime::is_nw() {
256                let filename = filename.as_ref().to_platform_string();
257                let options = Object::new();
258                let uint8_array = Uint8Array::from(data);
259                let buffer = Buffer::from_uint8_array(&uint8_array);
260                node::fs::write_file_sync(&filename, buffer.into(), options)?;
261            } else if runtime::is_chrome_extension(){
262                return Err(Error::Custom("localStorage api is unavailable, you can use write_binary_with_options() for chrome.storage.local api.".to_string()));
263            }else{
264                let key_name = options.local_storage_key(filename.as_ref());
265                local_storage().set_item(&key_name, data.to_hex().as_str())?;
266            }
267
268            Ok(())
269        }
270
271        pub async fn remove_with_options<P : AsRef<Path>>(filename: P, options: Options) -> Result<()> {
272            if runtime::is_node() || runtime::is_nw() {
273                let filename = filename.as_ref().to_platform_string();
274                node::fs::unlink_sync(&filename)?;
275            } else {
276                let key_name = options.local_storage_key(filename.as_ref());
277                if runtime::is_chrome_extension(){
278                    ChromeStorage::remove_item(&key_name).await?;
279                }else{
280                    local_storage().remove_item(&key_name)?;
281                }
282            }
283            Ok(())
284        }
285
286        pub fn remove_with_options_sync<P : AsRef<Path>>(filename: P, options: Options) -> Result<()> {
287            if runtime::is_node() || runtime::is_nw() {
288                let filename = filename.as_ref().to_platform_string();
289                node::fs::unlink_sync(&filename)?;
290            } else if runtime::is_chrome_extension(){
291                return Err(Error::Custom("localStorage api is unavailable, you can use remove_with_options() for chrome.storage.local api.".to_string()));
292            }else{
293                let key_name = options.local_storage_key(filename.as_ref());
294                local_storage().remove_item(&key_name)?;
295            }
296            Ok(())
297        }
298
299        pub async fn rename<P : AsRef<Path>>(from: P, to: P) -> Result<()> {
300            if runtime::is_node() || runtime::is_nw() {
301                let from = from.as_ref().to_platform_string();
302                let to = to.as_ref().to_platform_string();
303                node::fs::rename_sync(&from,&to)?;
304                Ok(())
305            } else {
306                Err(Error::NotSupported)
307            }
308        }
309
310        pub fn rename_sync<P : AsRef<Path>>(from: P, to: P) -> Result<()> {
311            if runtime::is_node() || runtime::is_nw() {
312                let from = from.as_ref().to_platform_string();
313                let to = to.as_ref().to_platform_string();
314                node::fs::rename_sync(&from,&to)?;
315                Ok(())
316            } else {
317                Err(Error::NotSupported)
318            }
319        }
320
321        pub async fn create_dir_all<P : AsRef<Path>>(filename: P) -> Result<()> {
322            create_dir_all_sync(filename)
323        }
324
325        pub fn create_dir_all_sync<P : AsRef<Path>>(filename: P) -> Result<()> {
326            if runtime::is_node() || runtime::is_nw() {
327                let options = Object::new();
328                Reflect::set(&options, &JsValue::from("recursive"), &JsValue::from_bool(true))?;
329                let filename = filename.as_ref().to_platform_string();
330                node::fs::mkdir_sync(&filename, options)?;
331            }
332
333            Ok(())
334        }
335
336
337        async fn fetch_metadata(path: &str, entries : &mut [DirEntry]) -> std::result::Result<(),JsErrorData> {
338            for entry in entries.iter_mut() {
339                let path = format!("{}/{}",path, entry.file_name());
340                let metadata = node::fs::stat_sync(&path).unwrap();
341                entry.metadata = metadata.try_into().ok();
342            }
343
344            Ok(())
345        }
346
347        async fn readdir_impl(path: &Path, metadata : bool) -> std::result::Result<Vec<DirEntry>,JsErrorData> {
348            let path_string = path.to_string_lossy().to_string();
349            let files = node::fs::readdir(&path_string).await?;
350            let list = files.dyn_into::<js_sys::Array>().expect("readdir: expecting resulting entries to be an array");
351            let mut entries = list.to_vec().into_iter().map(|s| s.into()).collect::<Vec<DirEntry>>();
352
353            if metadata {
354                fetch_metadata(&path_string, &mut entries).await?; //.map_err(|e|e.to_string())?;
355            }
356
357            Ok(entries)
358        }
359
360        pub async fn readdir<P>(path: P, metadata : bool) -> Result<Vec<DirEntry>>
361        where P : AsRef<Path> + Send + 'static
362        {
363            // this is a hack to bypass JsFuture being !Send
364            // until I had a chance to setup a proper infrastructure
365            // to relay JS promises within Send contexts.
366            // we want to use async version of readdir to ensure
367            // our executor is not blocked.
368
369            use workflow_core::sendable::Sendable;
370            use workflow_core::task::dispatch;
371            use workflow_core::channel::oneshot;
372
373            if runtime::is_node() || runtime::is_nw() {
374
375                let (sender, receiver) = oneshot();
376                dispatch(async move {
377                    let path = path.as_ref();
378                    let result = readdir_impl(path, metadata).await;
379                    sender.send(Sendable(result)).await.unwrap();
380                });
381
382                Ok(receiver.recv().await.unwrap().unwrap()?)
383            } else if runtime::is_chrome_extension(){
384                let entries = ChromeStorage::keys().await?
385                    .into_iter()
386                    .map(DirEntry::from)
387                    .collect::<Vec<_>>();
388                Ok(entries)
389            } else{
390                let local_storage = local_storage();
391
392                let mut entries = vec![];
393                let length = local_storage.length().unwrap();
394                for i in 0..length {
395                    let key = local_storage.key(i)?;
396                    if let Some(key) = key {
397                        entries.push(DirEntry::from(key));
398                    }
399                }
400                Ok(entries)
401            }
402        }
403
404        // -----------------------------------------
405
406    } else {  // cfg_if - native platforms
407
408        // -----------------------------------------
409
410        /// Returns whether the file at `filename` exists.
411        pub async fn exists_with_options<P : AsRef<Path>>(filename: P, _options: Options) -> Result<bool> {
412            Ok(filename.as_ref().exists())
413        }
414
415        /// Synchronously returns whether the file at `filename` exists.
416        pub fn exists_with_options_sync<P : AsRef<Path>>(filename: P, _options: Options) -> Result<bool> {
417            Ok(filename.as_ref().exists())
418        }
419
420        /// Reads the entire contents of `filename` into a UTF-8 string.
421        pub async fn read_to_string_with_options<P : AsRef<Path>>(filename: P, _options: Options) -> Result<String> {
422            Ok(std::fs::read_to_string(filename)?)
423        }
424
425        /// Synchronously reads the entire contents of `filename` into a UTF-8 string.
426        pub fn read_to_string_with_options_sync<P : AsRef<Path>>(filename: P, _options: Options) -> Result<String> {
427            Ok(std::fs::read_to_string(filename)?)
428        }
429
430        /// Reads the entire contents of `filename` into a byte vector.
431        pub async fn read_binary_with_options<P : AsRef<Path>>(filename: P, _options: Options) -> Result<Vec<u8>> {
432            Ok(std::fs::read(filename)?)
433        }
434
435        /// Synchronously reads the entire contents of `filename` into a byte vector.
436        pub fn read_binary_with_options_sync<P : AsRef<Path>>(filename: P, _options: Options) -> Result<Vec<u8>> {
437            Ok(std::fs::read(filename)?)
438        }
439
440        /// Writes `text` to `filename`, replacing any existing contents.
441        pub async fn write_string_with_options<P : AsRef<Path>>(filename: P, _options: Options, text : &str) -> Result<()> {
442            Ok(std::fs::write(filename, text)?)
443        }
444
445        /// Synchronously writes `text` to `filename`, replacing any existing contents.
446        pub fn write_string_with_options_sync<P : AsRef<Path>>(filename: P, _options: Options, text : &str) -> Result<()> {
447            Ok(std::fs::write(filename, text)?)
448        }
449
450        /// Writes the bytes in `data` to `filename`, replacing any existing contents.
451        pub async fn write_binary_with_options<P : AsRef<Path>>(filename: P, _options: Options, data : &[u8]) -> Result<()> {
452            Ok(std::fs::write(filename, data)?)
453        }
454
455        /// Synchronously writes the bytes in `data` to `filename`, replacing any existing contents.
456        pub fn write_binary_with_options_sync<P : AsRef<Path>>(filename: P, _options: Options, data : &[u8]) -> Result<()> {
457            Ok(std::fs::write(filename, data)?)
458        }
459
460        /// Removes the file at `filename`.
461        pub async fn remove_with_options<P : AsRef<Path>>(filename: P, _options: Options) -> Result<()> {
462            std::fs::remove_file(filename)?;
463            Ok(())
464        }
465
466        /// Synchronously removes the file at `filename`.
467        pub fn remove_with_options_sync<P : AsRef<Path>>(filename: P, _options: Options) -> Result<()> {
468            std::fs::remove_file(filename)?;
469            Ok(())
470        }
471
472        /// Renames or moves the file from `from` to `to`.
473        pub async fn rename<P : AsRef<Path>>(from: P, to: P) -> Result<()> {
474            std::fs::rename(from,to)?;
475            Ok(())
476        }
477
478        /// Synchronously renames or moves the file from `from` to `to`.
479        pub fn rename_sync<P : AsRef<Path>>(from: P, to: P) -> Result<()> {
480            std::fs::rename(from,to)?;
481            Ok(())
482        }
483
484        /// Recursively creates the directory `dir` and any missing parent directories.
485        pub async fn create_dir_all<P : AsRef<Path>>(dir: P) -> Result<()> {
486            std::fs::create_dir_all(dir)?;
487            Ok(())
488        }
489
490        /// Synchronously recursively creates the directory `dir` and any missing parent directories.
491        pub fn create_dir_all_sync<P : AsRef<Path>>(dir: P) -> Result<()> {
492            std::fs::create_dir_all(dir)?;
493            Ok(())
494        }
495
496        /// Lists the entries in directory `path`, optionally including file
497        /// [`Metadata`] for each entry when `metadata` is `true`.
498        pub async fn readdir<P : AsRef<Path>>(path: P, metadata : bool) -> Result<Vec<DirEntry>> {
499            let entries = std::fs::read_dir(path.as_ref())?;
500
501            if metadata {
502                let mut list = Vec::new();
503                for de in entries {
504                    let de = de?;
505                    let metadata = std::fs::metadata(de.path())?;
506                    let dir_entry = DirEntry::from((de,metadata));
507                    list.push(dir_entry);
508                }
509                Ok(list)
510            } else {
511                Ok(entries.map(|r|r.map(|e|e.into())).collect::<std::result::Result<Vec<_>,_>>()?)
512            }
513        }
514
515    }
516
517}
518
519/// File metadata such as timestamps and size, abstracted across the native
520/// and Node.js file system backends.
521#[derive(Clone, Debug)]
522pub struct Metadata {
523    created: Option<u64>,
524    modified: Option<u64>,
525    accessed: Option<u64>,
526    len: Option<u64>,
527}
528
529impl Metadata {
530    /// Creation time as seconds since the Unix epoch, if available.
531    pub fn created(&self) -> Option<u64> {
532        self.created
533    }
534
535    /// Last modification time as seconds since the Unix epoch, if available.
536    pub fn modified(&self) -> Option<u64> {
537        self.modified
538    }
539
540    /// Last access time as seconds since the Unix epoch, if available.
541    pub fn accessed(&self) -> Option<u64> {
542        self.accessed
543    }
544
545    /// Size of the file in bytes, if available.
546    pub fn len(&self) -> Option<u64> {
547        self.len
548    }
549
550    /// Returns `Some(true)` when the file is known to be empty, mirroring [`Metadata::len`].
551    pub fn is_empty(&self) -> Option<bool> {
552        self.len.map(|len| len == 0)
553    }
554}
555
556impl From<std::fs::Metadata> for Metadata {
557    fn from(metadata: std::fs::Metadata) -> Self {
558        Metadata {
559            created: metadata.created().ok().map(|created| {
560                created
561                    .duration_since(std::time::UNIX_EPOCH)
562                    .unwrap()
563                    .as_secs()
564            }),
565            modified: metadata.modified().ok().map(|modified| {
566                modified
567                    .duration_since(std::time::UNIX_EPOCH)
568                    .unwrap()
569                    .as_secs()
570            }),
571            accessed: metadata.accessed().ok().map(|accessed| {
572                accessed
573                    .duration_since(std::time::UNIX_EPOCH)
574                    .unwrap()
575                    .as_secs()
576            }),
577            len: Some(metadata.len()),
578        }
579    }
580}
581
582impl TryFrom<JsValue> for Metadata {
583    type Error = Error;
584    fn try_from(metadata: JsValue) -> Result<Self> {
585        if metadata.is_undefined() {
586            return Err(Error::Metadata);
587        }
588        let created = Reflect::get(&metadata, &"birthtimeMs".into())
589            .ok()
590            .map(|v| (v.as_f64().unwrap() / 1000.0) as u64);
591        let modified = Reflect::get(&metadata, &"mtimeMs".into())
592            .ok()
593            .map(|v| (v.as_f64().unwrap() / 1000.0) as u64);
594        let accessed = Reflect::get(&metadata, &"atimeMs".into())
595            .ok()
596            .map(|v| (v.as_f64().unwrap() / 1000.0) as u64);
597        let len = Reflect::get(&metadata, &"size".into())
598            .ok()
599            .map(|v| v.as_f64().unwrap() as u64);
600
601        Ok(Metadata {
602            created,
603            modified,
604            accessed,
605            len,
606        })
607    }
608}
609
610/// A single entry returned when listing the contents of a directory.
611#[derive(Clone, Debug)]
612pub struct DirEntry {
613    file_name: String,
614    metadata: Option<Metadata>,
615}
616
617impl DirEntry {
618    /// Returns the entry's file name (without the directory path).
619    pub fn file_name(&self) -> &str {
620        &self.file_name
621    }
622
623    /// Returns the entry's [`Metadata`] when it was requested during the listing.
624    pub fn metadata(&self) -> Option<&Metadata> {
625        self.metadata.as_ref()
626    }
627}
628
629impl From<std::fs::DirEntry> for DirEntry {
630    fn from(de: std::fs::DirEntry) -> Self {
631        DirEntry {
632            file_name: de.file_name().to_string_lossy().to_string(),
633            metadata: None,
634        }
635    }
636}
637
638impl From<(std::fs::DirEntry, std::fs::Metadata)> for DirEntry {
639    fn from((de, metadata): (std::fs::DirEntry, std::fs::Metadata)) -> Self {
640        DirEntry {
641            file_name: de.file_name().to_string_lossy().to_string(),
642            metadata: Some(metadata.into()),
643        }
644    }
645}
646
647impl From<JsValue> for DirEntry {
648    fn from(de: JsValue) -> Self {
649        DirEntry {
650            file_name: de.as_string().unwrap(),
651            metadata: None,
652        }
653    }
654}
655
656impl From<String> for DirEntry {
657    fn from(s: String) -> Self {
658        DirEntry {
659            file_name: s,
660            metadata: None,
661        }
662    }
663}
664
665/// Check if a file exists
666pub async fn exists<P: AsRef<Path>>(filename: P) -> Result<bool> {
667    exists_with_options(filename, Options::default()).await
668}
669
670/// Check if a file exists
671pub fn exists_sync<P: AsRef<Path>>(filename: P) -> Result<bool> {
672    exists_with_options_sync(filename, Options::default())
673}
674
675/// Read file contents to a string. If using within the web browser
676/// environment, a local storage key with the name of the file
677/// will be used.
678pub async fn read_to_string(filename: &Path) -> Result<String> {
679    read_to_string_with_options(filename, Options::default()).await
680}
681
682/// Read file contents to a string. If using within the web browser
683/// environment, a local storage key with the name of the file
684/// will be used.
685pub fn read_to_string_sync(filename: &Path) -> Result<String> {
686    read_to_string_with_options_sync(filename, Options::default())
687}
688
689/// Read binary file contents to a `Vec<u8>`. If using within the web browser
690/// environment, a local storage key with the name of the file
691/// will be used and the data is assumed to be hex-encoded.
692pub async fn read(filename: &Path) -> Result<Vec<u8>> {
693    read_binary_with_options(filename, Options::default()).await
694}
695
696/// Read binary file contents to a `Vec<u8>`. If using within the web browser
697/// environment, a local storage key with the name of the file
698/// will be used and the data is assumed to be hex-encoded.
699pub fn read_sync(filename: &Path) -> Result<Vec<u8>> {
700    read_binary_with_options_sync(filename, Options::default())
701}
702
703/// Write a string to a text file. If using within the web browser
704/// environment, a local storage key with the name of the file
705/// will be used.
706pub async fn write_string(filename: &Path, text: &str) -> Result<()> {
707    write_string_with_options(filename, Options::default(), text).await
708}
709
710/// Write a string to a text file. If using within the web browser
711/// environment, a local storage key with the name of the file
712/// will be used.
713pub fn write_string_sync(filename: &Path, text: &str) -> Result<()> {
714    write_string_with_options_sync(filename, Options::default(), text)
715}
716
717/// Write a `Vec<u8>` to a binary file. If using within the web browser
718/// environment, a local storage key with the name of the file
719/// will be used and the data will be hex-encoded.
720pub async fn write(filename: &Path, data: &[u8]) -> Result<()> {
721    write_binary_with_options(filename, Options::default(), data).await
722}
723
724/// Write a `Vec<u8>` to a binary file. If using within the web browser
725/// environment, a local storage key with the name of the file
726/// will be used and the data will be hex-encoded.
727pub async fn write_sync(filename: &Path, data: &[u8]) -> Result<()> {
728    write_binary_with_options_sync(filename, Options::default(), data)
729}
730
731/// Remove the file at the given path. If using within the web browser
732/// environment, a local storage key with the name of the file
733/// will be removed.
734pub async fn remove(filename: &Path) -> Result<()> {
735    remove_with_options(filename, Options::default()).await
736}
737
738/// Remove the file at the given path. If using within the web browser
739/// environment, a local storage key with the name of the file
740/// will be removed.
741pub fn remove_sync(filename: &Path) -> Result<()> {
742    remove_with_options_sync(filename, Options::default())
743}
744
745/// Read text file and deserialized it using `serde-json`.
746pub async fn read_json_with_options<T>(filename: &Path, options: Options) -> Result<T>
747where
748    T: DeserializeOwned,
749{
750    let text = read_to_string_with_options(filename, options).await?;
751    Ok(serde_json::from_str(&text)?)
752}
753
754/// Read text file and deserialized it using `serde-json`.
755pub fn read_json_with_options_sync<T>(filename: &Path, options: Options) -> Result<T>
756where
757    T: DeserializeOwned,
758{
759    let text = read_to_string_with_options_sync(filename, options)?;
760    Ok(serde_json::from_str(&text)?)
761}
762
763/// Write a serializable value to a text file using `serde-json`.
764pub async fn write_json_with_options<T>(filename: &Path, options: Options, value: &T) -> Result<()>
765where
766    T: Serialize,
767{
768    let json = serde_json::to_string(value)?;
769    write_string_with_options(filename, options, &json).await?;
770    Ok(())
771}
772
773/// Write a serializable value to a text file using `serde-json`.
774pub fn write_json_with_options_sync<T>(filename: &Path, options: Options, value: &T) -> Result<()>
775where
776    T: Serialize,
777{
778    let json = serde_json::to_string(value)?;
779    write_string_with_options_sync(filename, options, &json)?;
780    Ok(())
781}
782
783/// Read text file and deserialized it using `serde-json`.
784pub async fn read_json<T>(filename: &Path) -> Result<T>
785where
786    T: DeserializeOwned,
787{
788    read_json_with_options(filename, Options::default()).await
789}
790
791/// Read text file and deserialized it using `serde-json`.
792pub fn read_json_sync<T>(filename: &Path) -> Result<T>
793where
794    T: DeserializeOwned,
795{
796    read_json_with_options_sync(filename, Options::default())
797}
798
799/// Write a serializable value to a text file using `serde-json`.
800pub async fn write_json<T>(filename: &Path, value: &T) -> Result<()>
801where
802    T: Serialize,
803{
804    write_json_with_options(filename, Options::default(), value).await
805}
806
807/// Write a serializable value to a text file using `serde-json`.
808pub fn write_json_sync<T>(filename: &Path, value: &T) -> Result<()>
809where
810    T: Serialize,
811{
812    write_json_with_options_sync(filename, Options::default(), value)
813}
814
815/// Parses the supplied path resolving `~/` to the home directory.
816pub fn resolve_path(path: &str) -> Result<PathBuf> {
817    if let Some(_stripped) = path.strip_prefix("~/") {
818        if runtime::is_web() {
819            Ok(PathBuf::from(path))
820        } else if runtime::is_node() || runtime::is_nw() {
821            Ok(dirs::home_dir()
822                .ok_or_else(|| Error::HomeDir(path.to_string()))?
823                .join(_stripped))
824        } else {
825            cfg_if! {
826                if #[cfg(target_arch = "wasm32")] {
827                    Ok(PathBuf::from(path))
828                } else {
829                    Ok(home::home_dir().ok_or_else(||Error::HomeDir(path.to_string()))?.join(_stripped))
830                }
831            }
832        }
833    } else {
834        Ok(PathBuf::from(path))
835    }
836}
837
838/// Normalizes path, dereferencing relative references `.` and `..`
839/// and converting path separators to current platform separators.
840/// (detects platform natively or via NodeJS if operating in WASM32
841/// environment)
842pub trait NormalizePath {
843    /// Returns the normalized form of this path, resolving `.` and `..`
844    /// references and converting separators to the current platform.
845    fn normalize(&self) -> Result<PathBuf>;
846}
847
848impl NormalizePath for Path {
849    fn normalize(&self) -> Result<PathBuf> {
850        normalize(self)
851    }
852}
853
854impl NormalizePath for PathBuf {
855    fn normalize(&self) -> Result<PathBuf> {
856        normalize(self)
857    }
858}
859
860/// Convert path separators to unix or to current platform.
861/// Detects platform natively or using NodeJS if operating
862/// under WASM32 environment. Since in WASM32 paths default
863/// to forward slashes, when running WASM32 in Windows paths
864/// needs to be converted back and forth for various path-related
865/// functions to work.
866pub trait ToPlatform {
867    /// Returns this path with separators converted to those of the current platform.
868    fn to_platform(&self) -> PathBuf;
869    /// Returns this path as a string with current-platform separators.
870    fn to_platform_string(&self) -> String;
871    /// Returns this path with separators converted to unix forward slashes.
872    fn to_unix(&self) -> PathBuf;
873}
874
875impl ToPlatform for Path {
876    fn to_platform(&self) -> PathBuf {
877        if runtime::is_windows() {
878            convert_path_separators(self, "/", "\\")
879        } else {
880            self.to_path_buf()
881        }
882    }
883
884    fn to_platform_string(&self) -> String {
885        self.to_platform().to_string_lossy().to_string()
886    }
887
888    fn to_unix(&self) -> PathBuf {
889        if runtime::is_windows() {
890            convert_path_separators(self, "\\", "/")
891        } else {
892            self.to_path_buf()
893        }
894    }
895}
896
897/// Normalizes path, dereferencing relative references `.` and `..`
898/// and converting path separators to current platform separators.
899/// (detects platform natively or via NodeJS if operating in WASM32
900/// environment). Uses [`ToPlatform`] to perform path conversion.
901pub fn normalize<P>(path: P) -> Result<PathBuf>
902where
903    P: AsRef<Path>,
904{
905    let path = path.as_ref().to_unix();
906    let mut result = PathBuf::new();
907
908    for component in path.components() {
909        if let Some(c) = component.as_os_str().to_str() {
910            if c == "." {
911                continue;
912            } else if c == ".." {
913                result.pop();
914            } else {
915                result.push(c);
916            }
917        } else {
918            return Err(Error::InvalidPath(path.to_string_lossy().to_string()));
919        }
920    }
921
922    Ok(result.to_platform())
923}
924
925fn convert_path_separators<P>(path: P, from: &str, to: &str) -> PathBuf
926where
927    P: AsRef<Path>,
928{
929    let path = path.as_ref().to_string_lossy();
930    let path = path.replace(from, to);
931    PathBuf::from(path)
932}