use crate::shared::errors::ModError;
use glob::glob;
use std::{
fs::{self, File},
io::Read,
path::{self, Path, PathBuf},
};
#[derive(Debug, Clone)]
pub struct FileDefinition {
pub extension: String,
pub name: String,
pub size: u64,
pub is_folder: bool,
}
pub trait AbstractFileHandle {
fn exists(&mut self, needle: &str) -> bool;
fn is_folder(&self) -> bool;
fn list(&mut self) -> Vec<FileDefinition>;
fn as_text(&mut self, needle: &str) -> Result<String, std::io::Error>;
fn as_bin(&mut self, needle: &str) -> Result<Vec<u8>, std::io::Error>;
}
pub struct AbstractFolder {
path: PathBuf,
}
impl AbstractFolder {
pub fn new<P: AsRef<Path>>(file_path: P) -> Result<AbstractFolder, ModError> {
let input_path = file_path.as_ref();
if input_path.exists() {
if input_path.is_absolute() {
Ok(AbstractFolder {
path: input_path.to_path_buf(),
})
} else {
match path::absolute(input_path) {
Ok(new_path) => Ok(AbstractFolder { path: new_path }),
Err(..) => Err(ModError::FileErrorUnreadableZip),
}
}
} else {
Err(ModError::FileErrorUnreadableZip)
}
}
}
impl AbstractFileHandle for AbstractFolder {
fn as_text(&mut self, needle: &str) -> Result<String, std::io::Error> {
let search_path = Path::new(&self.path).join(needle);
fs::read_to_string(search_path)
}
fn as_bin(&mut self, needle: &str) -> Result<Vec<u8>, std::io::Error> {
let search_path = Path::new(&self.path).join(needle);
fs::read(search_path)
}
fn is_folder(&self) -> bool {
true
}
fn list(&mut self) -> Vec<FileDefinition> {
let mut names: Vec<FileDefinition> = vec![];
let search_path = self.path.clone().join("**/*").to_string_lossy().to_string();
let Ok(glob_entries) = glob(&search_path) else {
return names;
};
for entry in glob_entries.filter_map(Result::ok) {
let Ok(file_metadata) = std::fs::metadata(&entry) else {
continue;
};
let Ok(full_path) = path::absolute(entry) else {
continue;
};
let relative_path = match pathdiff::diff_paths(&full_path, &self.path) {
Some(good_path) => good_path.to_string_lossy().to_string(),
None => full_path.to_string_lossy().to_string(),
};
let extension = match full_path.extension() {
Some(ext) => ext.to_string_lossy().to_ascii_lowercase(),
None => String::new(),
};
names.push(FileDefinition {
extension,
is_folder: file_metadata.is_dir(),
name: relative_path.replace('\\', "/"),
size: file_metadata.len(),
});
}
names
}
fn exists(&mut self, needle: &str) -> bool {
let search_path = Path::new(&self.path).join(needle);
search_path.exists()
}
}
pub struct AbstractZipFile {
archive: zip::ZipArchive<File>,
}
impl AbstractZipFile {
pub fn new<P: AsRef<Path>>(file_path: P) -> Result<AbstractZipFile, ModError> {
let path = file_path.as_ref();
match std::fs::File::open(path) {
Ok(file) => match zip::ZipArchive::new(file) {
Ok(archive) => Ok(AbstractZipFile { archive }),
Err(..) => Err(ModError::FileErrorUnreadableZip),
},
Err(..) => Err(ModError::FileErrorUnreadableZip),
}
}
}
impl AbstractFileHandle for AbstractZipFile {
fn as_bin(&mut self, needle: &str) -> Result<Vec<u8>, std::io::Error> {
let mut file = self.archive.by_name(needle)?;
let mut buf = vec![];
file.read_to_end(&mut buf)?;
Ok(buf.clone())
}
fn as_text(&mut self, needle: &str) -> Result<String, std::io::Error> {
let mut file = self.archive.by_name(needle)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
fn is_folder(&self) -> bool {
false
}
fn list(&mut self) -> Vec<FileDefinition> {
let mut names: Vec<FileDefinition> = vec![];
for i in 0..self.archive.len() {
let Ok(file) = self.archive.by_index(i) else {
continue;
};
let name = file
.mangled_name()
.to_string_lossy()
.into_owned()
.replace('\\', "/");
let extension = match Path::new(&name).extension() {
Some(ext) => ext.to_string_lossy().to_ascii_lowercase(),
None => String::new(),
};
names.push(FileDefinition {
extension,
name,
size: if file.is_dir() { 0 } else { file.size() },
is_folder: file.is_dir(),
});
}
names
}
fn exists(&mut self, needle: &str) -> bool {
match self.archive.by_name(needle) {
Ok(..) => true,
Err(..) => false,
}
}
}
#[cfg(test)]
pub struct AbstractNull {}
#[cfg(test)]
impl AbstractNull {
pub fn new() -> Result<AbstractNull, ModError> {
Ok(AbstractNull {})
}
}
#[cfg(test)]
#[expect(unused_variables)]
impl AbstractFileHandle for AbstractNull {
fn as_text(&mut self, needle: &str) -> Result<String, std::io::Error> {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"not implemented",
))
}
fn as_bin(&mut self, needle: &str) -> Result<Vec<u8>, std::io::Error> {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"not implemented",
))
}
fn is_folder(&self) -> bool {
false
}
fn list(&mut self) -> Vec<FileDefinition> {
vec![]
}
fn exists(&mut self, needle: &str) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn null_adapter() {
let mut file_handle: Box<dyn AbstractFileHandle> = Box::new(AbstractNull::new().unwrap());
assert_eq!(file_handle.list().len(), 0);
assert_eq!(file_handle.is_folder(), false);
assert_eq!(file_handle.exists("foo.txt"), false);
assert!(file_handle.as_bin("foo.txt").is_err());
assert!(file_handle.as_text("foo.txt").is_err());
}
#[test]
fn invalid_path() {
let file_handle = AbstractFolder::new("./foo/bar/foo");
assert!(file_handle.is_err());
}
}