added filesystem abstraction layer and implemented basic filesystem - testing not complete yet, still features missing
Continuous integration / build (push) Has been cancelled

This commit is contained in:
2025-08-21 23:07:46 +01:00
parent c891a8be58
commit 9614d2884b
12 changed files with 477 additions and 348 deletions
+103
View File
@@ -0,0 +1,103 @@
use std::{
collections::HashMap,
io,
path::{Path, PathBuf},
sync::LazyLock,
};
use serde::{Serialize, de::DeserializeOwned};
#[cfg(feature = "native")]
use crate::PROJECT_FOLDER;
use crate::filesystem::native::NativeFileSystem;
#[cfg(feature = "native")]
pub mod native;
#[cfg(feature = "web")]
pub mod web;
pub static FILESYSTEM: LazyLock<NativeFileSystem> = LazyLock::new(|| {
#[cfg(feature = "native")]
return NativeFileSystem::new(PROJECT_FOLDER.clone());
#[cfg(feature = "web")]
return Box::new(web::WebFileSystem::new());
});
pub trait LegacyFileSystem {
fn read<T: DeserializeOwned>(&self, path: &Path) -> Result<T, FsError>;
fn read_bytes(&self, path: &Path) -> Result<Vec<u8>, FsError>;
fn write<T: Serialize>(&self, path: &Path, data: T) -> Result<(), FsError>;
fn delete(&self, path: &Path) -> Result<(), FsError>;
fn mkdir(&self, path: &Path) -> Result<(), FsError>;
fn rename(&self, path: &Path, new_path: &Path) -> Result<(), FsError>;
#[allow(unused)]
fn exists(&self, path: &Path) -> bool;
fn config_path(&self) -> PathBuf;
}
// ────────────────────────────────────────────────────────────────
// Custom error type
// ────────────────────────────────────────────────────────────────
#[derive(Debug)]
pub enum FsError {
Io(io::Error),
Serde(serde_json::Error),
}
impl std::fmt::Display for FsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FsError::Io(e) => write!(f, "IO error: {e}"),
FsError::Serde(e) => write!(f, "Serialization error: {e}"),
}
}
}
impl std::error::Error for FsError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
FsError::Io(e) => Some(e),
FsError::Serde(e) => Some(e),
}
}
}
// Convert the two underlying error types into our own
impl From<io::Error> for FsError {
fn from(err: io::Error) -> Self {
FsError::Io(err)
}
}
impl From<serde_json::Error> for FsError {
fn from(err: serde_json::Error) -> Self {
FsError::Serde(err)
}
}
#[allow(dead_code)]
pub struct Id(String);
#[allow(dead_code)]
pub trait FileSystem {
fn load<T: DeserializeOwned>(&self, id: Id) -> Result<T, FsError>;
fn save<T: Serialize>(&self, id: Id, data: T) -> Result<(), FsError>;
fn mkdir(&self, path: Path) -> Result<(), FsError>;
fn exists(&self, path: Path) -> bool;
}
#[allow(dead_code)]
pub struct Index {
file_cache: HashMap<Id, PathBuf>,
project_root: Directory,
}
#[allow(dead_code)]
pub struct Directory {
name: String,
id: Id,
children: HashMap<Id, Directory>,
files: Vec<Id>,
}
+105
View File
@@ -0,0 +1,105 @@
// ────────────────────────────────────────────────────────────────
// Imports
// ────────────────────────────────────────────────────────────────
use std::fs;
use std::io::{ErrorKind, Read};
use std::path::{Path, PathBuf};
use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::filesystem::{FsError, LegacyFileSystem};
// ────────────────────────────────────────────────────────────────
// Concrete implementation
// ────────────────────────────────────────────────────────────────
/// The concrete filesystem. All paths are interpreted relative to
/// `project_root`.
pub struct NativeFileSystem {
project_root: PathBuf,
}
impl NativeFileSystem {
/// Create a new instance.
pub fn new(root: impl Into<PathBuf>) -> Self {
Self {
project_root: root.into(),
}
}
/// Resolve the user supplied *relative* path against the project root.
#[inline]
fn full_path(&self, path: &Path) -> PathBuf {
self.project_root.join(path)
}
}
// ────────────────────────────────────────────────────────────────
// Implementation of the trait
// ────────────────────────────────────────────────────────────────
impl LegacyFileSystem for NativeFileSystem {
fn read<T: DeserializeOwned>(&self, path: &Path) -> Result<T, FsError> {
let full_path = self.full_path(path);
let file = fs::File::open(full_path).map_err(FsError::Io)?;
serde_json::from_reader(file).map_err(FsError::Serde)
}
fn read_bytes(&self, path: &Path) -> Result<Vec<u8>, FsError> {
let full_path = self.full_path(path);
let mut contents = Vec::new();
fs::File::open(full_path)?.read_to_end(&mut contents)?;
Ok(contents)
}
fn write<T: Serialize>(&self, path: &Path, data: T) -> Result<(), FsError> {
let full_path = self.full_path(path);
// Ensure the parent directory exists.
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent)?;
}
let file = fs::File::create(full_path)?;
serde_json::to_writer(file, &data).map_err(FsError::Serde)
}
fn delete(&self, path: &Path) -> Result<(), FsError> {
let full_path = self.full_path(path);
match fs::remove_file(&full_path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == ErrorKind::IsADirectory => {
// Remove a directory tree.
fs::remove_dir_all(full_path).map_err(FsError::Io)
}
Err(e) => Err(FsError::Io(e)),
}
}
fn mkdir(&self, path: &Path) -> Result<(), FsError> {
let full_path = self.full_path(path);
fs::create_dir_all(full_path).map_err(FsError::Io)
}
fn exists(&self, path: &Path) -> bool {
let full_path = self.full_path(path);
full_path.exists()
}
fn rename(&self, path: &Path, other: &Path) -> Result<(), FsError> {
let full_path = self.full_path(path);
let full_other = self.full_path(other);
fs::rename(full_path, full_other).map_err(FsError::Io)
}
fn config_path(&self) -> PathBuf {
match std::env::var("HOME") {
Ok(path) => PathBuf::from(path + "/.config/worldcoder/settings.json"),
Err(_) => {
eprintln!(
"XDG_CONFIG_HOME not set, using default path of ~/.config/worldcoder/settings.json"
);
"~/.config/worldcoder/settings.json".into()
}
}
}
}