use {
crate::fs::errors::{
FsError,
Result,
},
error_stack::ResultExt,
std::{
io::{
Read,
Write,
},
sync::Arc,
time::SystemTime,
},
};
pub trait FileSystem: std::fmt::Debug + Send + Sync {
fn uri(&self) -> crate::Uri;
fn open(
&self,
path: &crate::Uri,
options: OpenOptions,
) -> Result<Box<dyn FileHandle>>;
fn read(&self, path: &crate::Uri) -> Result<Vec<u8>>;
fn read_to_new_rope(&self, path: &crate::Uri) -> Result<ropey::Rope>;
fn read_to_string(&self, path: &crate::Uri) -> Result<String> {
let bytes = self.read(path)?;
String::from_utf8(bytes).map_err(|e| {
error_stack::Report::new(FsError::Io(format!("Invalid UTF-8 data: {e}")))
})
}
fn iter_files(
&self,
) -> Result<Box<dyn Iterator<Item = (crate::Uri, Vec<u8>)> + '_>>;
fn write(&self, path: &crate::Uri, data: &[u8]) -> Result<()>;
fn write_str(&self, path: &crate::Uri, data: &str) -> Result<()>;
fn append(&self, path: &crate::Uri, data: &[u8]) -> Result<()>;
fn delete(&self, path: &crate::Uri) -> Result<()>;
fn metadata(&self, path: &crate::Uri) -> Result<Metadata>;
fn root(&self) -> Result<Box<dyn DirEntry>>;
fn dir(&self, path: &crate::Uri) -> Result<Box<dyn DirEntry>>;
fn find(
&self,
path: &crate::Uri,
glob: &[String],
) -> Result<im::Vector<Arc<dyn DirEntry>>>;
fn add_tree_to_snapshot(
&self,
title: &str,
snapshot: &mut ferrotype::Ferrotype,
) {
match self.iter_files() {
| Ok(files) => {
let files_vec: Vec<_> = files.collect();
if files_vec.is_empty() {
snapshot.add(title, "(empty filesystem)".to_string());
} else {
snapshot.add(title, format!("Found {} files:", files_vec.len()));
for (url, file_content) in files_vec {
let filename = url
.path_segments()
.and_then(|mut segments| segments.next_back())
.unwrap_or("unknown");
let file_title = format!(
"{} ({}, {} bytes)",
filename,
url.path(),
file_content.len()
);
let file_body = if is_likely_text(&file_content) {
match std::str::from_utf8(&file_content) {
| Ok(text) => {
if text.is_empty() {
"(empty file)".to_string()
} else {
text.to_string()
}
},
| Err(_) => "<invalid UTF-8>".to_string(),
}
} else {
"<binary data>".to_string()
};
snapshot.add(&file_title, file_body);
}
}
},
| Err(err) => {
let error_content = format!("Error reading filesystem: {err}");
snapshot.add(title, error_content);
},
}
}
}
fn is_likely_text(content: &[u8]) -> bool {
if content.is_empty() {
return true;
}
let null_bytes = content.iter().filter(|&&b| b == 0).count();
if null_bytes > 0 {
return false;
}
let non_printable = content
.iter()
.filter(|&&b| b < 32 && b != b'\n' && b != b'\r' && b != b'\t')
.count();
non_printable < content.len() / 10
}
#[derive(Debug, Clone)]
pub struct Metadata {
pub is_dir: bool,
pub len: u64,
pub modified: SystemTime,
pub created: SystemTime,
pub readonly: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct Permissions {
pub readonly: bool,
}
pub trait DirEntry: std::fmt::Debug {
fn path(&self) -> crate::Uri;
fn file_type(&self) -> FileType;
fn is_dir(&self) -> bool {
self.file_type() == FileType::Directory
}
fn is_file(&self) -> bool {
self.file_type() == FileType::File
}
fn metadata(&self) -> Result<Metadata>;
fn entry(&mut self, entry: Box<dyn DirEntry>);
fn write(&mut self, path: &crate::Uri, data: &[u8]) -> Result<()>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileType {
File,
Directory,
}
#[derive(Debug, Clone, Copy)]
pub struct OpenOptions {
pub read: bool,
pub write: bool,
pub create: bool,
pub append: bool,
pub truncate: bool,
}
impl Default for OpenOptions {
fn default() -> Self {
OpenOptions {
read: true,
write: false,
create: false,
append: false,
truncate: false,
}
}
}
pub trait FileHandle: Read + Write + Send + Sync {
fn metadata(&self) -> Result<Metadata>;
}
pub(crate) fn build_glob_set(patterns: &[String]) -> Result<globset::GlobSet> {
let mut builder = globset::GlobSetBuilder::new();
for pattern in patterns {
builder.add(
globset::GlobBuilder::new(pattern)
.literal_separator(true)
.build()
.change_context(FsError::InvalidGlob(
"Invalid glob pattern".to_string(),
))
.attach_printable_lazy(|| format!("Pattern: {pattern}"))?,
);
}
builder.build().change_context(FsError::InvalidGlob(
"Failed to build glob pattern".to_string(),
))
}