2021-01-27 18:20:47 +00:00
|
|
|
use std::fmt;
|
|
|
|
use std::io;
|
|
|
|
use std::io::SeekFrom;
|
|
|
|
use std::path;
|
2021-02-12 10:05:28 +00:00
|
|
|
|
2021-01-27 18:20:47 +00:00
|
|
|
use crate::framework::context::Context;
|
2021-02-12 10:05:28 +00:00
|
|
|
use crate::framework::error::{GameError, GameResult};
|
|
|
|
use crate::framework::vfs;
|
|
|
|
use crate::framework::vfs::{OpenOptions, VFS};
|
2021-01-27 18:20:47 +00:00
|
|
|
|
|
|
|
/// A structure that contains the filesystem state and cache.
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct Filesystem {
|
|
|
|
vfs: vfs::OverlayFS,
|
|
|
|
user_vfs: vfs::OverlayFS,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Represents a file, either in the filesystem, or in the resources zip file,
|
|
|
|
/// or whatever.
|
|
|
|
pub enum File {
|
|
|
|
/// A wrapper for a VFile trait object.
|
|
|
|
VfsFile(Box<dyn vfs::VFile>),
|
|
|
|
}
|
|
|
|
|
2021-02-12 10:05:28 +00:00
|
|
|
unsafe impl Send for File {}
|
|
|
|
|
2021-01-27 18:20:47 +00:00
|
|
|
impl fmt::Debug for File {
|
|
|
|
// Make this more useful?
|
|
|
|
// But we can't seem to get a filename out of a file,
|
|
|
|
// soooooo.
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
match *self {
|
|
|
|
File::VfsFile(ref _file) => write!(f, "VfsFile"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl io::Read for File {
|
|
|
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
|
|
match *self {
|
|
|
|
File::VfsFile(ref mut f) => f.read(buf),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl io::Write for File {
|
|
|
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
|
|
match *self {
|
|
|
|
File::VfsFile(ref mut f) => f.write(buf),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn flush(&mut self) -> io::Result<()> {
|
|
|
|
match *self {
|
|
|
|
File::VfsFile(ref mut f) => f.flush(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl io::Seek for File {
|
|
|
|
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
|
|
|
match *self {
|
|
|
|
File::VfsFile(ref mut f) => f.seek(pos),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Filesystem {
|
|
|
|
pub fn new() -> Filesystem {
|
|
|
|
// Set up VFS to merge resource path, root path, and zip path.
|
|
|
|
let overlay = vfs::OverlayFS::new();
|
|
|
|
// User data VFS.
|
2021-02-10 11:53:49 +00:00
|
|
|
let user_overlay = vfs::OverlayFS::new();
|
2021-01-27 18:20:47 +00:00
|
|
|
|
|
|
|
Filesystem {
|
|
|
|
vfs: overlay,
|
|
|
|
user_vfs: user_overlay,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Opens the given `path` and returns the resulting `File`
|
|
|
|
/// in read-only mode.
|
2021-02-12 10:05:28 +00:00
|
|
|
pub(crate) fn open<P: AsRef<path::Path>>(&self, path: P) -> GameResult<File> {
|
2021-01-27 18:20:47 +00:00
|
|
|
self.vfs.open(path.as_ref()).map(|f| File::VfsFile(f))
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Opens the given `path` from user directory and returns the resulting `File`
|
|
|
|
/// in read-only mode.
|
2021-02-12 10:05:28 +00:00
|
|
|
pub(crate) fn user_open<P: AsRef<path::Path>>(&self, path: P) -> GameResult<File> {
|
2021-01-27 18:20:47 +00:00
|
|
|
self.user_vfs.open(path.as_ref()).map(|f| File::VfsFile(f))
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Opens a file in the user directory with the given
|
|
|
|
/// [`filesystem::OpenOptions`](struct.OpenOptions.html).
|
|
|
|
/// Note that even if you open a file read-write, it can only
|
|
|
|
/// write to files in the "user" directory.
|
2021-02-12 10:05:28 +00:00
|
|
|
pub(crate) fn open_options<P: AsRef<path::Path>>(&self, path: P, options: OpenOptions) -> GameResult<File> {
|
2021-01-27 18:20:47 +00:00
|
|
|
self.user_vfs
|
|
|
|
.open_options(path.as_ref(), options)
|
|
|
|
.map(|f| File::VfsFile(f))
|
|
|
|
.map_err(|e| {
|
2021-02-12 10:05:28 +00:00
|
|
|
GameError::ResourceLoadError(format!("Tried to open {:?} but got error: {:?}", path.as_ref(), e))
|
2021-01-27 18:20:47 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Creates a new file in the user directory and opens it
|
|
|
|
/// to be written to, truncating it if it already exists.
|
2021-02-12 10:05:28 +00:00
|
|
|
pub(crate) fn user_create<P: AsRef<path::Path>>(&self, path: P) -> GameResult<File> {
|
2021-01-27 18:20:47 +00:00
|
|
|
self.user_vfs.create(path.as_ref()).map(|f| File::VfsFile(f))
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Create an empty directory in the user dir
|
|
|
|
/// with the given name. Any parents to that directory
|
|
|
|
/// that do not exist will be created.
|
2021-02-12 10:05:28 +00:00
|
|
|
pub(crate) fn user_create_dir<P: AsRef<path::Path>>(&self, path: P) -> GameResult<()> {
|
2021-01-27 18:20:47 +00:00
|
|
|
self.user_vfs.mkdir(path.as_ref())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Deletes the specified file in the user dir.
|
2021-02-12 10:05:28 +00:00
|
|
|
pub(crate) fn user_delete<P: AsRef<path::Path>>(&self, path: P) -> GameResult<()> {
|
2021-01-27 18:20:47 +00:00
|
|
|
self.user_vfs.rm(path.as_ref())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Deletes the specified directory in the user dir,
|
|
|
|
/// and all its contents!
|
2021-02-12 10:05:28 +00:00
|
|
|
pub(crate) fn user_delete_dir<P: AsRef<path::Path>>(&self, path: P) -> GameResult<()> {
|
2021-01-27 18:20:47 +00:00
|
|
|
self.user_vfs.rmrf(path.as_ref())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Check whether a file or directory in the user directory exists.
|
|
|
|
pub(crate) fn user_exists<P: AsRef<path::Path>>(&self, path: P) -> bool {
|
|
|
|
self.user_vfs.exists(path.as_ref())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Check whether a file or directory exists.
|
|
|
|
pub(crate) fn exists<P: AsRef<path::Path>>(&self, path: P) -> bool {
|
|
|
|
self.vfs.exists(path.as_ref())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Check whether a path points at a file.
|
|
|
|
pub(crate) fn user_is_file<P: AsRef<path::Path>>(&self, path: P) -> bool {
|
|
|
|
self.user_vfs
|
|
|
|
.metadata(path.as_ref())
|
|
|
|
.map(|m| m.is_file())
|
|
|
|
.unwrap_or(false)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Check whether a path points at a file.
|
|
|
|
pub(crate) fn is_file<P: AsRef<path::Path>>(&self, path: P) -> bool {
|
2021-02-12 10:05:28 +00:00
|
|
|
self.vfs.metadata(path.as_ref()).map(|m| m.is_file()).unwrap_or(false)
|
2021-01-27 18:20:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Check whether a path points at a directory.
|
|
|
|
pub(crate) fn user_is_dir<P: AsRef<path::Path>>(&self, path: P) -> bool {
|
|
|
|
self.user_vfs
|
|
|
|
.metadata(path.as_ref())
|
|
|
|
.map(|m| m.is_dir())
|
|
|
|
.unwrap_or(false)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Check whether a path points at a directory.
|
|
|
|
pub(crate) fn is_dir<P: AsRef<path::Path>>(&self, path: P) -> bool {
|
2021-02-12 10:05:28 +00:00
|
|
|
self.vfs.metadata(path.as_ref()).map(|m| m.is_dir()).unwrap_or(false)
|
2021-01-27 18:20:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns a list of all files and directories in the user directory,
|
|
|
|
/// in no particular order.
|
|
|
|
///
|
|
|
|
/// Lists the base directory if an empty path is given.
|
|
|
|
pub(crate) fn user_read_dir<P: AsRef<path::Path>>(
|
2021-02-12 10:05:28 +00:00
|
|
|
&self,
|
2021-01-27 18:20:47 +00:00
|
|
|
path: P,
|
2021-02-12 10:05:28 +00:00
|
|
|
) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
|
|
|
|
let itr = self
|
|
|
|
.user_vfs
|
|
|
|
.read_dir(path.as_ref())?
|
|
|
|
.map(|fname| fname.expect("Could not read file in read_dir()? Should never happen, I hope!"));
|
2021-01-27 18:20:47 +00:00
|
|
|
Ok(Box::new(itr))
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns a list of all files and directories in the resource directory,
|
|
|
|
/// in no particular order.
|
|
|
|
///
|
|
|
|
/// Lists the base directory if an empty path is given.
|
|
|
|
pub(crate) fn read_dir<P: AsRef<path::Path>>(
|
2021-02-12 10:05:28 +00:00
|
|
|
&self,
|
2021-01-27 18:20:47 +00:00
|
|
|
path: P,
|
2021-02-12 10:05:28 +00:00
|
|
|
) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
|
|
|
|
let itr = self
|
|
|
|
.vfs
|
|
|
|
.read_dir(path.as_ref())?
|
|
|
|
.map(|fname| fname.expect("Could not read file in read_dir()? Should never happen, I hope!"));
|
2021-01-27 18:20:47 +00:00
|
|
|
Ok(Box::new(itr))
|
|
|
|
}
|
|
|
|
|
2021-02-12 10:05:28 +00:00
|
|
|
fn write_to_string(&self) -> String {
|
2021-01-27 18:20:47 +00:00
|
|
|
use std::fmt::Write;
|
|
|
|
let mut s = String::new();
|
|
|
|
for vfs in self.vfs.roots() {
|
|
|
|
write!(s, "Source {:?}", vfs).expect("Could not write to string; should never happen?");
|
|
|
|
match vfs.read_dir(path::Path::new("/")) {
|
|
|
|
Ok(files) => {
|
|
|
|
for itm in files {
|
2021-02-12 10:05:28 +00:00
|
|
|
write!(s, " {:?}", itm).expect("Could not write to string; should never happen?");
|
2021-01-27 18:20:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Err(e) => write!(s, " Could not read source: {:?}", e)
|
|
|
|
.expect("Could not write to string; should never happen?"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
s
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Adds the given (absolute) path to the list of directories
|
|
|
|
/// it will search to look for resources.
|
|
|
|
///
|
|
|
|
/// You probably shouldn't use this in the general case, since it is
|
|
|
|
/// harder than it looks to make it bulletproof across platforms.
|
|
|
|
/// But it can be very nice for debugging and dev purposes, such as
|
|
|
|
/// by pushing `$CARGO_MANIFEST_DIR/resources` to it
|
|
|
|
pub fn mount(&mut self, path: &path::Path, readonly: bool) {
|
|
|
|
let physfs = vfs::PhysicalFS::new(path, readonly);
|
|
|
|
trace!("Mounting new path: {:?}", physfs);
|
|
|
|
self.vfs.push_back(Box::new(physfs));
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn mount_vfs(&mut self, vfs: Box<dyn vfs::VFS>) {
|
|
|
|
self.vfs.push_back(vfs);
|
|
|
|
}
|
2021-01-28 22:33:43 +00:00
|
|
|
|
|
|
|
pub fn mount_user_vfs(&mut self, vfs: Box<dyn vfs::VFS>) {
|
|
|
|
self.user_vfs.push_back(vfs);
|
|
|
|
}
|
2021-01-27 18:20:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Opens the given path and returns the resulting `File`
|
|
|
|
/// in read-only mode.
|
2021-02-12 10:05:28 +00:00
|
|
|
pub fn open<P: AsRef<path::Path>>(ctx: &Context, path: P) -> GameResult<File> {
|
2021-01-27 18:20:47 +00:00
|
|
|
ctx.filesystem.open(path)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Opens the given path in the user directory and returns the resulting `File`
|
|
|
|
/// in read-only mode.
|
2021-02-12 10:05:28 +00:00
|
|
|
pub fn user_open<P: AsRef<path::Path>>(ctx: &Context, path: P) -> GameResult<File> {
|
2021-01-27 18:20:47 +00:00
|
|
|
ctx.filesystem.user_open(path)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Opens a file in the user directory with the given `filesystem::OpenOptions`.
|
2021-02-12 10:05:28 +00:00
|
|
|
pub fn open_options<P: AsRef<path::Path>>(ctx: &Context, path: P, options: OpenOptions) -> GameResult<File> {
|
2021-01-27 18:20:47 +00:00
|
|
|
ctx.filesystem.open_options(path, options)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Creates a new file in the user directory and opens it
|
|
|
|
/// to be written to, truncating it if it already exists.
|
2021-02-12 10:05:28 +00:00
|
|
|
pub fn user_create<P: AsRef<path::Path>>(ctx: &Context, path: P) -> GameResult<File> {
|
2021-01-27 18:20:47 +00:00
|
|
|
ctx.filesystem.user_create(path)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Create an empty directory in the user dir
|
|
|
|
/// with the given name. Any parents to that directory
|
|
|
|
/// that do not exist will be created.
|
2021-02-12 10:05:28 +00:00
|
|
|
pub fn user_create_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> GameResult {
|
2021-01-27 18:20:47 +00:00
|
|
|
ctx.filesystem.user_create_dir(path.as_ref())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Deletes the specified file in the user dir.
|
2021-02-12 10:05:28 +00:00
|
|
|
pub fn user_delete<P: AsRef<path::Path>>(ctx: &Context, path: P) -> GameResult {
|
2021-01-27 18:20:47 +00:00
|
|
|
ctx.filesystem.user_delete(path.as_ref())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Deletes the specified directory in the user dir,
|
|
|
|
/// and all its contents!
|
2021-02-12 10:05:28 +00:00
|
|
|
pub fn user_delete_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> GameResult {
|
2021-01-27 18:20:47 +00:00
|
|
|
ctx.filesystem.user_delete_dir(path.as_ref())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Check whether a file or directory exists.
|
|
|
|
pub fn user_exists<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
|
|
|
|
ctx.filesystem.user_exists(path.as_ref())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Check whether a path points at a file.
|
|
|
|
pub fn user_is_file<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
|
|
|
|
ctx.filesystem.user_is_file(path)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Check whether a path points at a directory.
|
|
|
|
pub fn user_is_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
|
|
|
|
ctx.filesystem.user_is_dir(path)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns a list of all files and directories in the user directory,
|
|
|
|
/// in no particular order.
|
|
|
|
///
|
|
|
|
/// Lists the base directory if an empty path is given.
|
|
|
|
pub fn user_read_dir<P: AsRef<path::Path>>(
|
2021-02-12 10:05:28 +00:00
|
|
|
ctx: &Context,
|
2021-01-27 18:20:47 +00:00
|
|
|
path: P,
|
2021-02-12 10:05:28 +00:00
|
|
|
) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
|
2021-01-27 18:20:47 +00:00
|
|
|
ctx.filesystem.user_read_dir(path)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Check whether a file or directory exists.
|
|
|
|
pub fn exists<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
|
|
|
|
ctx.filesystem.exists(path.as_ref())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Check whether a path points at a file.
|
|
|
|
pub fn is_file<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
|
|
|
|
ctx.filesystem.is_file(path)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Check whether a path points at a directory.
|
|
|
|
pub fn is_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
|
|
|
|
ctx.filesystem.is_dir(path)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Returns a list of all files and directories in the resource directory,
|
|
|
|
/// in no particular order.
|
|
|
|
///
|
|
|
|
/// Lists the base directory if an empty path is given.
|
2021-02-12 10:05:28 +00:00
|
|
|
pub fn read_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
|
2021-01-27 18:20:47 +00:00
|
|
|
ctx.filesystem.read_dir(path)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Adds the given (absolute) path to the list of directories
|
|
|
|
/// it will search to look for resources.
|
|
|
|
///
|
|
|
|
/// You probably shouldn't use this in the general case, since it is
|
|
|
|
/// harder than it looks to make it bulletproof across platforms.
|
|
|
|
/// But it can be very nice for debugging and dev purposes, such as
|
|
|
|
/// by pushing `$CARGO_MANIFEST_DIR/resources` to it
|
|
|
|
pub fn mount(ctx: &mut Context, path: &path::Path, readonly: bool) {
|
|
|
|
ctx.filesystem.mount(path, readonly)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Adds a VFS to the list of resource search locations.
|
|
|
|
pub fn mount_vfs(ctx: &mut Context, vfs: Box<dyn vfs::VFS>) {
|
|
|
|
ctx.filesystem.mount_vfs(vfs)
|
2021-01-28 22:33:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Adds a VFS to the list of user data search locations.
|
|
|
|
pub fn mount_user_vfs(ctx: &mut Context, vfs: Box<dyn vfs::VFS>) {
|
|
|
|
ctx.filesystem.mount_user_vfs(vfs)
|
|
|
|
}
|