use {
super::*,
crate::{
fs::errors::{
FsError,
IoErrorExt,
Result,
},
uri::Uri,
},
error_stack::Report,
ignore::WalkBuilder,
std::{
fs::{
self,
File,
OpenOptions as FsOpenOptions,
},
io::{
Read,
Write,
},
path::{
Path,
PathBuf,
},
time::SystemTime,
},
};
#[derive(Clone)]
pub struct PhysicalFileSystem {
root: crate::Uri,
}
#[derive(Clone)]
struct PhysicalDirEntry {
path: crate::Uri,
metadata: std::fs::Metadata,
}
impl std::fmt::Debug for PhysicalDirEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "PhysicalDirEntry({})", self.path)
}
}
impl std::hash::Hash for PhysicalDirEntry {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.path.hash(state);
}
}
struct PhysicalFileHandle {
file: File,
}
impl PhysicalFileSystem {
#[allow(clippy::new_ret_no_self)]
pub fn new(root: crate::Uri) -> Result<FS> {
if root.scheme().as_str() != "file" {
return Err(
Report::new(FsError::invalid_operation(
"PhysicalFileSystem only supports file:// URIs",
))
.attach_printable(format!("URI: {root}")),
);
}
Ok(FS::from(Self { root }))
}
pub fn new_from_string(root: &str) -> Result<FS> {
Ok(FS::from(Self {
root: Self::path_to_uri(Path::new(root))?,
}))
}
pub fn new_with_path(root: &std::path::Path) -> Result<FS> {
Ok(FS::from(Self {
root: Self::path_to_uri(root)?,
}))
}
fn uri_to_path(&self, uri: &crate::Uri) -> Result<PathBuf> {
if uri.scheme().as_str() != "file" {
return Err(
Report::new(FsError::invalid_operation(
"PhysicalFileSystem only supports file:// URIs",
))
.attach_printable(format!("URI: {uri}")),
);
}
uri
.to_file_path()
.map(|cow| cow.into_owned())
.ok_or_else(|| {
Report::new(FsError::path_conversion_error("Invalid file URI"))
.attach_printable(format!("URI: {uri}"))
})
}
fn path_to_uri(path: &Path) -> Result<crate::Uri> {
Uri::from_file_path(path).ok_or_else(|| {
Report::new(FsError::path_conversion_error("Invalid file path"))
.attach_printable(format!("Path: {}", path.display()))
})
}
fn convert_metadata(metadata: std::fs::Metadata) -> Metadata {
Metadata {
is_dir: metadata.is_dir(),
len: metadata.len(),
modified: metadata.modified().unwrap_or(SystemTime::now()),
created: metadata.created().unwrap_or(SystemTime::now()),
readonly: metadata.permissions().readonly(),
}
}
}
impl FileSystem for PhysicalFileSystem {
fn uri(&self) -> crate::Uri {
self.root.clone()
}
fn open(
&self,
path: &crate::Uri,
options: OpenOptions,
) -> Result<Box<dyn FileHandle>> {
let path_buf = self.uri_to_path(path)?;
let file = FsOpenOptions::new()
.read(options.read)
.write(options.write)
.create(options.create)
.append(options.append)
.truncate(options.truncate)
.open(&path_buf)
.map_err(|e| e.to_fs_error_with_path(&path_buf))?;
Ok(Box::new(PhysicalFileHandle { file }))
}
fn read(&self, path: &crate::Uri) -> Result<Vec<u8>> {
let path_buf = self.uri_to_path(path)?;
fs::read(&path_buf).map_err(|e| e.to_fs_error_with_path(&path_buf))
}
fn read_to_new_rope(&self, path: &crate::Uri) -> Result<ropey::Rope> {
let path_buf = self.uri_to_path(path)?;
let file =
File::open(&path_buf).map_err(|e| e.to_fs_error_with_path(&path_buf))?;
ropey::Rope::from_reader(file).map_err(|e| {
Report::new(FsError::io_error(format!("Failed to create rope: {e}")))
.attach_printable(format!("Path: {}", path_buf.display()))
})
}
fn write(&self, path: &crate::Uri, data: &[u8]) -> Result<()> {
let path_buf = self.uri_to_path(path)?;
if let Some(parent) = path_buf.parent() {
fs::create_dir_all(parent)
.map_err(|e| e.to_fs_error_with_path(parent))?;
}
fs::write(&path_buf, data).map_err(|e| {
e.to_fs_error_with_path(&path_buf)
.attach_printable(format!("Failed to write {} bytes", data.len()))
})
}
fn write_str(&self, path: &crate::Uri, data: &str) -> Result<()> {
self.write(path, data.as_bytes())
}
fn append(&self, path: &crate::Uri, data: &[u8]) -> Result<()> {
let path_buf = self.uri_to_path(path)?;
if let Some(parent) = path_buf.parent() {
fs::create_dir_all(parent)
.map_err(|e| e.to_fs_error_with_path(parent))?;
}
let mut file = FsOpenOptions::new()
.append(true)
.create(true)
.open(&path_buf)
.map_err(|e| e.to_fs_error_with_path(&path_buf))?;
file.write_all(data).map_err(|e| {
e.to_fs_error_with_path(&path_buf)
.attach_printable(format!("Failed to append {} bytes", data.len()))
})
}
fn delete(&self, path: &crate::Uri) -> Result<()> {
let path_buf = self.uri_to_path(path)?;
if path_buf.is_dir() {
fs::remove_dir_all(&path_buf).map_err(|e| {
e.to_fs_error_with_path(&path_buf)
.attach_printable("Failed to remove directory recursively")
})
} else {
fs::remove_file(&path_buf).map_err(|e| {
e.to_fs_error_with_path(&path_buf)
.attach_printable("Failed to remove file")
})
}
}
fn metadata(&self, path: &crate::Uri) -> Result<Metadata> {
let path_buf = self.uri_to_path(path)?;
let metadata = fs::metadata(&path_buf).map_err(|e| {
e.to_fs_error_with_path(&path_buf)
.attach_printable("Failed to get file metadata")
})?;
Ok(Self::convert_metadata(metadata))
}
fn root(&self) -> Result<Box<dyn DirEntry>> {
let path = self.uri_to_path(&self.root)?;
let metadata = fs::metadata(&path).map_err(|e| {
e.to_fs_error_with_path(&path)
.attach_printable("Failed to get root metadata")
})?;
Ok(Box::new(PhysicalDirEntry {
path: self.root.clone(),
metadata,
}))
}
fn dir(&self, path: &crate::Uri) -> Result<Box<dyn DirEntry>> {
let path_buf = self.uri_to_path(path)?;
fs::create_dir_all(&path_buf).map_err(|e| {
e.to_fs_error_with_path(&path_buf)
.attach_printable("Failed to create directory")
})?;
let metadata = fs::metadata(&path_buf).map_err(|e| {
e.to_fs_error_with_path(&path_buf)
.attach_printable("Failed to get directory metadata")
})?;
Ok(Box::new(PhysicalDirEntry {
path: path.clone(),
metadata,
}))
}
fn find(
&self,
path: &crate::Uri,
globs: &[String],
) -> Result<im::Vector<Arc<dyn DirEntry>>> {
let glob = crate::fs::filesystem::build_glob_set(globs)?;
let path_buf = self.uri_to_path(path)?;
let root_path = self.uri_to_path(&self.root)?;
let walker = WalkBuilder::new(&path_buf)
.filter_entry(move |entry| {
entry.path().is_dir()
|| glob.is_match({
entry
.path()
.strip_prefix(&root_path)
.unwrap_or(entry.path())
})
})
.build();
let mut entries: im::Vector<Arc<dyn DirEntry>> = im::Vector::new();
for entry_result in walker {
match entry_result {
| Ok(entry) => {
let entry_path = entry.path();
if let Ok(metadata) = entry.metadata()
&& !metadata.is_dir()
{
let entry_uri = Self::path_to_uri(entry_path)?;
entries.push_back(Arc::new(PhysicalDirEntry {
path: entry_uri,
metadata,
}) as Arc<dyn DirEntry>);
}
},
| Err(err) => {
return Err(
Report::new(FsError::filesystem_error("Walk error"))
.attach_printable(format!("Failed to walk directory: {err}"))
.attach_printable(format!("Path: {path_buf:?}")),
);
},
}
}
Ok(entries)
}
fn iter_files(
&self,
) -> Result<Box<dyn Iterator<Item = (crate::Uri, Vec<u8>)> + '_>> {
let root_path = self.uri_to_path(&self.root)?;
let walker = WalkBuilder::new(&root_path)
.hidden(false)
.git_ignore(true)
.build();
let file_iter = walker.filter_map(move |entry| {
let entry = entry.ok()?;
let path = entry.path();
if !entry.file_type()?.is_file() {
return None;
}
let uri = PhysicalFileSystem::path_to_uri(path).ok()?;
let content = std::fs::read(path).ok()?;
Some((uri, content))
});
Ok(Box::new(file_iter))
}
}
impl std::fmt::Debug for PhysicalFileSystem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "PhysicalFileSystem({})", self.root)
}
}
impl DirEntry for PhysicalDirEntry {
fn path(&self) -> Uri {
self.path.clone()
}
fn file_type(&self) -> FileType {
if self.metadata.is_dir() {
FileType::Directory
} else {
FileType::File
}
}
fn metadata(&self) -> Result<Metadata> {
Ok(PhysicalFileSystem::convert_metadata(self.metadata.clone()))
}
fn entry(&mut self, _entry: Box<dyn DirEntry>) {
}
fn write(&mut self, path: &Uri, data: &[u8]) -> Result<()> {
if !self.is_dir() {
return Err(
Report::new(FsError::invalid_operation(
"Cannot write to a non-directory entry",
))
.attach_printable(format!("Path: {}", self.path)),
);
}
let fs = PhysicalFileSystem::new(self.path.clone())?;
fs.write(path, data)
}
}
impl FileHandle for PhysicalFileHandle {
fn metadata(&self) -> Result<Metadata> {
let metadata = self.file.metadata().map_err(|e| {
e.to_fs_error()
.attach_printable("Failed to get file handle metadata")
})?;
Ok(PhysicalFileSystem::convert_metadata(metadata))
}
}
impl Read for PhysicalFileHandle {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.file.read(buf)
}
}
impl Write for PhysicalFileHandle {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.file.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.file.flush()
}
}