use std::io;
use std::path::Path;
use std::io::Write;
use cap_std::ambient_authority;
use cap_std::fs::Dir;
use cap_tempfile::TempFile;
use crate::paths::ProjectPath;
pub struct ProjectDir {
dir: Dir,
}
impl ProjectDir {
pub fn open(root: &Path) -> io::Result<Self> {
let dir = Dir::open_ambient_dir(root, ambient_authority())?;
Ok(Self { dir })
}
pub fn read(&self, path: &ProjectPath) -> io::Result<Vec<u8>> {
self.dir.read(path.to_native())
}
pub fn read_text(&self, path: &ProjectPath) -> io::Result<String> {
let bytes = self.read(path)?;
String::from_utf8(bytes).map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
}
#[must_use]
pub fn is_file(&self, path: &ProjectPath) -> bool {
self.dir.is_file(path.to_native())
}
#[must_use]
pub fn is_dir(&self, path: &ProjectPath) -> bool {
path.as_str().is_empty() || self.dir.is_dir(path.to_native())
}
#[must_use]
pub fn exists(&self, path: &ProjectPath) -> bool {
self.dir.exists(path.to_native())
}
pub fn file_len(&self, path: &ProjectPath) -> io::Result<u64> {
Ok(self.dir.metadata(path.to_native())?.len())
}
pub fn symlink_component(&self, path: &ProjectPath) -> io::Result<Option<ProjectPath>> {
for ancestor in path.ancestors() {
match self.dir.symlink_metadata(ancestor.to_native()) {
Ok(metadata) if metadata.is_symlink() => return Ok(Some(ancestor)),
Ok(_) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error),
}
}
Ok(None)
}
pub fn walk(&self, directory: &ProjectPath) -> io::Result<Vec<ProjectPath>> {
let mut found = Vec::new();
self.walk_into(directory, &mut found)?;
found.sort();
Ok(found)
}
fn walk_into(&self, directory: &ProjectPath, found: &mut Vec<ProjectPath>) -> io::Result<()> {
let mut entries = Vec::new();
for entry in self.dir.read_dir(directory.to_native())? {
let entry = entry?;
let name = entry.file_name();
let Some(name) = name.to_str() else {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"`{directory}` contains an entry whose name is not valid UTF-8: {}",
name.to_string_lossy()
),
));
};
let file_type = entry.file_type()?;
entries.push((name.to_owned(), file_type));
}
entries.sort_by(|a, b| a.0.cmp(&b.0));
for (name, file_type) in entries {
let segment = ProjectPath::parse(&name).map_err(|error| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("`{directory}` contains an entry that is not a portable path segment: {error}"),
)
})?;
let path = directory.join(&segment);
if file_type.is_symlink() {
continue;
}
if file_type.is_dir() {
self.walk_into(&path, found)?;
} else if file_type.is_file() {
found.push(path);
}
}
Ok(())
}
pub fn write_atomic(&self, path: &ProjectPath, bytes: &[u8]) -> io::Result<()> {
let parent = path.parent();
if !parent.as_str().is_empty() {
self.dir.create_dir_all(parent.to_native())?;
}
let directory = if parent.as_str().is_empty() {
self.dir.try_clone()?
} else {
self.dir.open_dir(parent.to_native())?
};
let mut temp = TempFile::new(&directory)?;
temp.write_all(bytes)?;
temp.as_file_mut().sync_data()?;
temp.replace(path.file_name())
}
pub fn remove_file(&self, path: &ProjectPath) -> io::Result<()> {
self.dir.remove_file(path.to_native())
}
pub fn subdir(&self, path: &ProjectPath) -> io::Result<Dir> {
self.dir.open_dir(path.to_native())
}
}