use std::{
ffi::OsStr,
fmt::Debug,
fs,
io::{self},
path::{Path, PathBuf},
time::SystemTime,
};
use log::warn;
use crate::vfs::{DirTree, FileMeta, VfsNode};
use super::{LocalDirError, LocalSyncInfo};
pub(crate) trait LocalPath: Debug {
type DirEntry: LocalPath;
fn is_file(&self) -> bool;
fn is_dir(&self) -> bool;
fn file_name(&self) -> Option<&OsStr>;
fn read_dir(&self) -> io::Result<impl Iterator<Item = io::Result<Self::DirEntry>>>;
fn modification_time(&self) -> io::Result<SystemTime>;
fn file_size(&self) -> io::Result<u64>;
}
impl LocalPath for Path {
type DirEntry = PathBuf;
fn is_file(&self) -> bool {
self.is_file()
}
fn is_dir(&self) -> bool {
self.is_dir()
}
fn file_name(&self) -> Option<&OsStr> {
self.file_name()
}
fn read_dir(&self) -> io::Result<impl Iterator<Item = io::Result<Self::DirEntry>>> {
fs::read_dir(self).map(|entry_iter| {
entry_iter
.into_iter()
.map(|entry_res| entry_res.map(|entry| entry.path()))
})
}
fn modification_time(&self) -> io::Result<SystemTime> {
self.metadata()?.modified()
}
fn file_size(&self) -> io::Result<u64> {
Ok(self.metadata()?.len())
}
}
impl LocalPath for PathBuf {
type DirEntry = Self;
fn is_file(&self) -> bool {
LocalPath::is_file(self.as_path())
}
fn is_dir(&self) -> bool {
LocalPath::is_dir(self.as_path())
}
fn file_name(&self) -> Option<&OsStr> {
LocalPath::file_name(self.as_path())
}
fn read_dir(&self) -> io::Result<impl Iterator<Item = io::Result<Self::DirEntry>>> {
LocalPath::read_dir(self.as_path())
}
fn modification_time(&self) -> io::Result<SystemTime> {
LocalPath::modification_time(self.as_path())
}
fn file_size(&self) -> io::Result<u64> {
LocalPath::file_size(self.as_path())
}
}
pub(crate) fn node_from_path_rec<P: LocalPath>(
parent: &mut DirTree<LocalSyncInfo>,
children: &[P],
) -> Result<(), LocalDirError> {
for path in children {
let sync = match path.modification_time() {
Ok(time) => LocalSyncInfo::new(time.into()),
Err(err) => {
warn!("skipping file {path:?}: {err}");
continue;
}
};
if path.is_file() {
let node = VfsNode::File(FileMeta::new(
path.file_name()
.and_then(|s| s.to_str())
.ok_or(LocalDirError::invalid_path(path))?,
path.file_size().map_err(|e| LocalDirError::io(path, e))?,
sync,
));
parent.insert_child(node);
} else if path.is_dir() {
let mut node = DirTree::new(
path.file_name()
.and_then(|s| s.to_str())
.ok_or(LocalDirError::invalid_path(path))?,
sync,
);
node_from_path_rec(
&mut node,
&path
.read_dir()
.map_err(|e| LocalDirError::io(path, e))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| LocalDirError::io(path, e))?,
)?;
parent.insert_child(VfsNode::Dir(node));
} else {
return Err(LocalDirError::invalid_path(path));
}
}
Ok(())
}
#[cfg(test)]
mod test {
use chrono::Utc;
use super::*;
use crate::vfs::{DirTree, VfsNode};
use crate::test_utils::TestNode::{D, F, L};
#[test]
fn test_parse_path() {
let base = vec![
D("Doc", vec![F("f1.md"), F("f2.pdf")]),
D("a", vec![D("b", vec![D("c", vec![])])]),
];
let mut root = DirTree::new("", LocalSyncInfo::new(Utc::now()));
node_from_path_rec(&mut root, &base).unwrap();
let parsed = VfsNode::Dir(root);
let reference = &D("", base).into_node();
assert!(parsed.structural_eq(reference));
}
#[test]
fn test_parse_symlink() {
let target = D("Doc", vec![F("f1.md"), F("f2.pdf")]);
let base = vec![L("link", Some(&target))];
let mut root = DirTree::new("", LocalSyncInfo::new(Utc::now()));
node_from_path_rec(&mut root, &base).unwrap();
let parsed = VfsNode::Dir(root);
let reference = D("", vec![D("link", vec![F("f1.md"), F("f2.pdf")])]).into_node();
assert!(parsed.structural_eq(&reference));
let target = F("f1.md");
let base = vec![D("Doc", vec![L("link", Some(&target)), F("f2.pdf")])];
let mut root = DirTree::new("", LocalSyncInfo::new(Utc::now()));
node_from_path_rec(&mut root, &base).unwrap();
let parsed = VfsNode::Dir(root);
let reference = D("", vec![D("Doc", vec![F("link"), F("f2.pdf")])]).into_node();
assert!(parsed.structural_eq(&reference));
let base = vec![D("Doc", vec![L("link", None), F("f2.pdf")])];
let mut root = DirTree::new("", LocalSyncInfo::new(Utc::now()));
node_from_path_rec(&mut root, &base).unwrap();
let parsed = VfsNode::Dir(root);
let reference = D("", vec![D("Doc", vec![F("f2.pdf")])]).into_node();
assert!(parsed.structural_eq(&reference));
}
}