1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

//! Types and functions related to file system directory management.

use serde::Serialize;
use std::{
  fs::{self, metadata, symlink_metadata},
  path::{Path, PathBuf},
};
use tempfile::{self, tempdir};

/// A disk entry which is either a file or a directory.
///
/// This is the result of the [`read_dir`]. The `children` field is always `Some` if the entry is a directory.
#[derive(Debug, Serialize)]
#[non_exhaustive]
pub struct DiskEntry {
  /// The path to the entry.
  pub path: PathBuf,
  /// The name of the entry (file name with extension or directory name).
  pub name: Option<String>,
  /// The children of this entry if it's a directory.
  #[serde(skip_serializing_if = "Option::is_none")]
  pub children: Option<Vec<DiskEntry>>,
}

/// Checks if the given path is a directory.
pub fn is_dir<P: AsRef<Path>>(path: P) -> crate::api::Result<bool> {
  metadata(path).map(|md| md.is_dir()).map_err(Into::into)
}

fn is_symlink<P: AsRef<Path>>(path: P) -> crate::api::Result<bool> {
  // TODO: remove the different implementation once we raise tauri's MSRV to at least 1.58
  #[cfg(windows)]
  let ret = symlink_metadata(path)
    .map(|md| md.is_symlink())
    .map_err(Into::into);

  #[cfg(not(windows))]
  let ret = symlink_metadata(path)
    .map(|md| md.file_type().is_symlink())
    .map_err(Into::into);

  ret
}

/// Reads a directory. Can perform recursive operations.
pub fn read_dir<P: AsRef<Path>>(path: P, recursive: bool) -> crate::api::Result<Vec<DiskEntry>> {
  read_dir_with_options(path, recursive, ReadDirOptions { scope: None })
}

#[derive(Clone, Copy)]
pub(crate) struct ReadDirOptions<'a> {
  pub scope: Option<&'a crate::FsScope>,
}

pub(crate) fn read_dir_with_options<P: AsRef<Path>>(
  path: P,
  recursive: bool,
  options: ReadDirOptions<'_>,
) -> crate::api::Result<Vec<DiskEntry>> {
  let mut files_and_dirs: Vec<DiskEntry> = vec![];
  for entry in fs::read_dir(path)? {
    let path = entry?.path();
    let path_as_string = path.display().to_string();

    if let Ok(flag) = is_dir(&path_as_string) {
      files_and_dirs.push(DiskEntry {
        path: path.clone(),
        children: if flag {
          Some(
            if recursive
              && (!is_symlink(&path_as_string)?
                || options.scope.map(|s| s.is_allowed(&path)).unwrap_or(true))
            {
              read_dir_with_options(&path_as_string, true, options)?
            } else {
              vec![]
            },
          )
        } else {
          None
        },
        name: path
          .file_name()
          .map(|name| name.to_string_lossy())
          .map(|name| name.to_string()),
      });
    }
  }
  Result::Ok(files_and_dirs)
}

/// Runs a closure with a temporary directory argument.
pub fn with_temp_dir<F: FnOnce(&tempfile::TempDir)>(callback: F) -> crate::api::Result<()> {
  let dir = tempdir()?;
  callback(&dir);
  dir.close()?;
  Ok(())
}

#[cfg(test)]
mod test {
  use super::*;
  use quickcheck_macros::quickcheck;
  use std::{ffi::OsStr, path::PathBuf};

  // check is dir function by passing in arbitrary strings
  #[quickcheck]
  fn qc_is_dir(f: String) -> bool {
    // if the string runs through is_dir and comes out as an OK result then it must be a DIR.
    if is_dir(f.clone()).is_ok() {
      PathBuf::from(f).is_dir()
    } else {
      true
    }
  }

  fn name_from_path(path: PathBuf) -> Option<String> {
    path
      .file_name()
      .map(|name| name.to_string_lossy())
      .map(|name| name.to_string())
  }

  #[test]
  // check the read_dir function with recursive = true
  fn check_read_dir_recursively() {
    // define a relative directory string test/api/
    let dir = PathBuf::from("test/api/");
    // add the files to this directory
    let mut file_one = dir.clone();
    file_one.push("test.txt");
    let mut file_two = dir.clone();
    file_two.push("test_binary");

    // call walk_dir on the directory
    let res = read_dir(dir, true);

    // assert that the result is Ok()
    assert!(res.is_ok());

    // destruct the OK into a vector of DiskEntry Structs
    if let Ok(vec) = res {
      // assert that the vector length is only 3
      assert_eq!(vec.len(), 2);

      // get the first DiskEntry
      let first = &vec[0];
      // get the second DiskEntry
      let second = &vec[1];

      if first.path.extension() == Some(OsStr::new("txt")) {
        // check the fields for the first DiskEntry
        assert_eq!(first.path, file_one);
        assert!(first.children.is_none());
        assert_eq!(first.name, name_from_path(file_one));

        // check the fields for the third DiskEntry
        assert_eq!(second.path, file_two);
        assert!(second.children.is_none());
        assert_eq!(second.name, name_from_path(file_two));
      } else {
        // check the fields for the second DiskEntry
        assert_eq!(first.path, file_two);
        assert!(first.children.is_none());
        assert_eq!(first.name, name_from_path(file_two));

        // check the fields for the third DiskEntry
        assert_eq!(second.path, file_one);
        assert!(second.children.is_none());
        assert_eq!(second.name, name_from_path(file_one));
      }
    }
  }

  #[test]
  // check the read_dir function with recursive = false
  fn check_read_dir() {
    // define a relative directory test/api/
    let dir = PathBuf::from("test/api/");

    // call list_dir_contents on the dir
    let res = read_dir(dir, false);

    // assert that the result is Ok()
    assert!(res.is_ok());

    // destruct the vector from the Ok()
    if let Ok(vec) = res {
      // assert the length of the vector is 2
      assert_eq!(vec.len(), 2);

      // get the two DiskEntry structs in this vector
      let first = &vec[0];
      let second = &vec[1];

      if first.path.extension() == Some(OsStr::new("txt")) {
        // check the fields for the first DiskEntry
        assert_eq!(first.path, PathBuf::from("test/api/test.txt"));
        assert!(first.children.is_none());
        assert_eq!(first.name, Some("test.txt".to_string()));

        // check the fields for the second DiskEntry
        assert_eq!(second.path, PathBuf::from("test/api/test_binary"));
        assert!(second.children.is_none());
        assert_eq!(second.name, Some("test_binary".to_string()));
      } else {
        // check the fields for the first DiskEntry
        assert_eq!(second.path, PathBuf::from("test/api/test.txt"));
        assert!(second.children.is_none());
        assert_eq!(second.name, Some("test.txt".to_string()));

        // check the fields for the second DiskEntry
        assert_eq!(first.path, PathBuf::from("test/api/test_binary"));
        assert!(first.children.is_none());
        assert_eq!(first.name, Some("test_binary".to_string()));
      }
    }
  }

  #[test]
  // test the with_temp_dir function
  fn check_test_dir() {
    // create a callback closure that takes in a TempDir type and prints it.
    let callback = |td: &tempfile::TempDir| {
      println!("{:?}", td);
    };

    // execute the with_temp_dir function on the callback
    let res = with_temp_dir(callback);

    // assert that the result is an OK type.
    assert!(res.is_ok());
  }
}