use anyhow::Result;
use std::path::{Component, Path, PathBuf};
use tokio::fs::OpenOptions;
use super::file::File;
#[cfg(target_os = "windows")]
const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x02000000;
#[derive(Debug)]
pub struct Directory {
path: PathBuf,
}
impl Directory {
pub fn path(&self) -> PathBuf {
self.path.clone()
}
}
#[derive(Debug)]
pub enum Entry {
File(Box<File>),
Directory(Directory),
}
#[derive(Clone)]
pub struct ScopedFileSystem {
pub root: PathBuf,
}
impl ScopedFileSystem {
pub fn new(root: PathBuf) -> Result<Self> {
Ok(ScopedFileSystem { root })
}
pub async fn resolve(&self, path: PathBuf) -> std::io::Result<Entry> {
let entry_path = self.build_relative_path(path);
ScopedFileSystem::open(entry_path).await
}
fn build_relative_path(&self, path: PathBuf) -> PathBuf {
let mut root = self.root.clone();
root.extend(&ScopedFileSystem::normalize_path(&path));
root
}
fn normalize_path(path: &Path) -> PathBuf {
path.components()
.fold(PathBuf::new(), |mut result, p| match p {
Component::ParentDir => {
result.pop();
result
}
Component::Normal(os_string) => {
result.push(os_string);
result
}
_ => result,
})
}
#[cfg(not(target_os = "windows"))]
async fn open(path: PathBuf) -> std::io::Result<Entry> {
let mut open_options = OpenOptions::new();
let entry_path: PathBuf = path.clone();
let file = open_options.read(true).open(path).await?;
let metadata = file.metadata().await?;
if metadata.is_dir() {
return Ok(Entry::Directory(Directory { path: entry_path }));
}
Ok(Entry::File(Box::new(File::new(entry_path, file, metadata))))
}
#[cfg(target_os = "windows")]
async fn open(path: PathBuf) -> std::io::Result<Entry> {
let mut open_options = OpenOptions::new();
let entry_path: PathBuf = path.clone();
let file = open_options
.read(true)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS)
.open(path)
.await?;
let metadata = file.metadata().await?;
if metadata.is_dir() {
return Ok(Entry::Directory(Directory { path: entry_path }));
}
Ok(Entry::File(Box::new(File::new(entry_path, file, metadata))))
}
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
#[test]
fn builds_a_relative_path_to_root_from_provided_path() {
let sfs = ScopedFileSystem::new(PathBuf::from("")).unwrap();
let mut root_path = sfs.root.clone();
let file_path = PathBuf::from(".github/ISSUE_TEMPLATE/feature-request.md");
root_path.push(file_path);
let resolved_path =
sfs.build_relative_path(PathBuf::from(".github/ISSUE_TEMPLATE/feature-request.md"));
assert_eq!(root_path, resolved_path);
}
#[test]
fn builds_a_relative_path_to_root_from_forward_slash() {
let sfs = ScopedFileSystem::new(PathBuf::from("")).unwrap();
let resolved_path = sfs.build_relative_path(PathBuf::from("/"));
assert_eq!(sfs.root, resolved_path);
}
#[test]
fn builds_a_relative_path_to_root_from_backwards() {
let sfs = ScopedFileSystem::new(PathBuf::from("")).unwrap();
let resolved_path = sfs.build_relative_path(PathBuf::from("../../"));
assert_eq!(sfs.root, resolved_path);
}
#[test]
fn builds_an_invalid_path_if_an_arbitrary_path_is_provided() {
let sfs = ScopedFileSystem::new(PathBuf::from("")).unwrap();
let resolved_path =
sfs.build_relative_path(PathBuf::from("unexistent_dir/confidential/recipe.txt"));
assert_ne!(sfs.root, resolved_path);
}
#[test]
fn normalizes_an_arbitrary_path() {
let arbitrary_path = PathBuf::from("docs/collegue/cs50/lectures/../code/voting_excecise");
let normalized = ScopedFileSystem::normalize_path(&arbitrary_path);
assert_eq!(
normalized.to_str().unwrap(),
"docs/collegue/cs50/code/voting_excecise"
);
}
#[tokio::test]
async fn resolves_a_file() {
let sfs = ScopedFileSystem::new(PathBuf::from("")).unwrap();
let resolved_entry = sfs.resolve(PathBuf::from("assets/logo.svg")).await.unwrap();
if let Entry::File(file) = resolved_entry {
assert!(file.metadata.is_file());
} else {
panic!("Found a directory instead of a file in the provied path");
}
}
#[tokio::test]
async fn detect_directory_paths() {
let sfs = ScopedFileSystem::new(PathBuf::from("")).unwrap();
let resolved_entry = sfs.resolve(PathBuf::from("assets/")).await.unwrap();
assert!(matches!(resolved_entry, Entry::Directory(_)));
}
#[tokio::test]
async fn detect_directory_paths_without_postfixed_slash() {
let sfs = ScopedFileSystem::new(PathBuf::from("")).unwrap();
let resolved_entry = sfs.resolve(PathBuf::from("assets")).await.unwrap();
assert!(matches!(resolved_entry, Entry::Directory(_)));
}
#[tokio::test]
async fn returns_error_if_file_doesnt_exists() {
let sfs = ScopedFileSystem::new(PathBuf::from("")).unwrap();
let resolved_entry = sfs
.resolve(PathBuf::from("assets/unexistent_file.doc"))
.await;
assert!(resolved_entry.is_err());
}
}