millennium 1.0.0-beta.3

Create consistent, light, & secure apps that work on all platforms, using HTML, CSS, and JavaScript
Documentation
// Copyright 2022 pyke.io
//           2019-2021 Tauri Programme within The Commons Conservancy
//                     [https://tauri.studio/]
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

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

use std::{
	fs::{self, metadata},
	path::{Path, PathBuf}
};

use serde::Serialize;
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)
}

/// Reads a directory. Can perform recursive operations.
pub fn read_dir<P: AsRef<Path>>(path: P, recursive: bool) -> 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 { read_dir(&path_as_string, true)? } 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 std::{ffi::OsStr, path::PathBuf};

	use quickcheck_macros::quickcheck;

	use super::*;

	// 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());
	}
}