mirror of
https://github.com/kwsch/pkNX.git
synced 2026-05-03 03:15:21 -05:00
Still need to start using it, but want to do it a bit slow to test it out. Also should really build some unit tests for this one..
221 lines
8.5 KiB
C#
221 lines
8.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text;
|
|
|
|
namespace pkNX.Containers.VFS;
|
|
|
|
public class LayeredFileSystem : IFileSystem
|
|
{
|
|
public IReadOnlyList<IFileSystem> FileSystems { get; }
|
|
|
|
public LayeredFileSystem(IReadOnlyList<IFileSystem> fileSystems)
|
|
{
|
|
FileSystems = fileSystems;
|
|
|
|
Debug.Assert(FileSystems.Any(), "No filesystems provided.");
|
|
Debug.Assert(FileSystems.Any(fs => !fs.IsReadOnly), "Should contain at least one writable filesystem.");
|
|
}
|
|
|
|
public LayeredFileSystem(params IFileSystem[] fileSystems) :
|
|
this(fileSystems.AsReadOnly())
|
|
{ }
|
|
|
|
public void Dispose()
|
|
{
|
|
foreach (var fs in FileSystems)
|
|
fs.Dispose();
|
|
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
public IEnumerable<FileSystemPath> GetEntityPaths(FileSystemPath path, Func<FileSystemPath, bool>? filter = null)
|
|
{
|
|
var entities = new HashSet<FileSystemPath>();
|
|
foreach (var fs in FileSystems.Where(fs => fs.Exists(path)))
|
|
entities.UnionWith(fs.GetEntityPaths(path, filter));
|
|
return entities;
|
|
}
|
|
|
|
public IEnumerable<FileSystemPath> GetDirectoryPaths(FileSystemPath path, Func<FileSystemPath, bool>? filter = null)
|
|
{
|
|
var directories = new HashSet<FileSystemPath>();
|
|
foreach (var fs in FileSystems.Where(fs => fs.Exists(path)))
|
|
directories.UnionWith(fs.GetDirectoryPaths(path, filter));
|
|
return directories;
|
|
}
|
|
|
|
public IEnumerable<FileSystemPath> GetFilePaths(FileSystemPath path, Func<FileSystemPath, bool>? filter = null)
|
|
{
|
|
var files = new HashSet<FileSystemPath>();
|
|
foreach (var fs in FileSystems.Where(fs => fs.Exists(path)))
|
|
files.UnionWith(fs.GetFilePaths(path, filter));
|
|
return files;
|
|
}
|
|
|
|
public IFileSystem GetFirst()
|
|
{
|
|
return FileSystems.First();
|
|
}
|
|
|
|
public IFileSystem GetFirstWritable()
|
|
{
|
|
return FileSystems.First(fs => !fs.IsReadOnly);
|
|
}
|
|
|
|
public bool Exists(FileSystemPath path)
|
|
{
|
|
return FileSystems.Any(fs => fs.Exists(path));
|
|
}
|
|
|
|
public IFileSystem? GetFirstWhereExists(FileSystemPath path)
|
|
{
|
|
return FileSystems.FirstOrDefault(fs => fs.Exists(path));
|
|
}
|
|
|
|
public IFileSystem? GetFirstWritableWhereExists(FileSystemPath path)
|
|
{
|
|
return FileSystems.FirstOrDefault(fs => !fs.IsReadOnly && fs.Exists(path));
|
|
}
|
|
|
|
private bool ValidateOpenMode(FileMode mode = FileMode.Open, FileAccess access = FileAccess.Read)
|
|
{
|
|
if (!access.HasFlag(FileAccess.Write) && mode is FileMode.Create or FileMode.CreateNew or FileMode.Truncate or FileMode.Append)
|
|
{
|
|
throw new ArgumentException($"File mode '{mode}' requires files to be accessed with write permission, but the access mode was '{access}'", nameof(access));
|
|
}
|
|
|
|
if (access.HasFlag(FileAccess.Read) && mode == FileMode.Append)
|
|
{
|
|
throw new ArgumentException("File mode 'Append' requires files to be accessed with in read/write permission.", nameof(access));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public Stream OpenFile(FileSystemPath path, FileMode mode = FileMode.Open, FileAccess access = FileAccess.Read)
|
|
{
|
|
if (!ValidateOpenMode(mode, access))
|
|
return Stream.Null;
|
|
|
|
switch (mode)
|
|
{
|
|
case FileMode.Open:
|
|
{
|
|
// just read from top layer
|
|
IFileSystem? fs = GetFirstWhereExists(path);
|
|
if (fs == null)
|
|
throw new FileNotFoundException($"Could not find the file at the specified path: {path}", nameof(path));
|
|
|
|
return fs.OpenFile(path, FileMode.Open, access);
|
|
}
|
|
case FileMode.OpenOrCreate:
|
|
{
|
|
// - Specifies that the operating system should open a file if it exists; otherwise, a new file should be created.
|
|
// If the file is opened with FileAccess.Read, Read permission is required.
|
|
// If the file access is FileAccess.Write, Write permission is required.
|
|
// If the file is opened with FileAccess.ReadWrite, both Read and Write permissions are required.
|
|
|
|
IFileSystem? fs = GetFirstWhereExists(path);
|
|
if (fs != null)
|
|
{
|
|
// The file exists, now we need to check if we can open it with the requested access
|
|
// readonly fs requires special handling for write requests.
|
|
// if readonly access is requested, we can open it if the fs is readonly
|
|
var readStream = fs.OpenFile(path);
|
|
if (!fs.IsReadOnly || access == FileAccess.Read)
|
|
return readStream;
|
|
|
|
// For write-only access, we can just create a new empty file
|
|
IFileSystem writableFs = GetFirstWritable();
|
|
writableFs.CreateDirectoryRecursive(path.ParentPath);
|
|
var writeStream = writableFs.CreateFile(path);
|
|
|
|
if (access == FileAccess.Write)
|
|
return writeStream;
|
|
|
|
// For read-write access, we need to first copy the file to a writable fs
|
|
readStream.CopyTo(writeStream);
|
|
writeStream.Seek(0, SeekOrigin.Begin);
|
|
readStream.Dispose();
|
|
return writeStream;
|
|
}
|
|
|
|
// The file does not exist, create it on the first writable fs
|
|
return GetFirstWritable().CreateFile(path);
|
|
}
|
|
case FileMode.CreateNew:
|
|
{
|
|
// Specifies that the operating system should create a new file.
|
|
// - Requires Write permission.
|
|
// - Requires the file does not already exist.
|
|
|
|
if (Exists(path))
|
|
throw new IOException($"File {path.Path} already exists.");
|
|
|
|
return GetFirstWritable().CreateFile(path);
|
|
}
|
|
case FileMode.Create:
|
|
{
|
|
// - This requires Write permission.
|
|
// - FileMode.Create is equivalent to requesting that if the file does not exist, use CreateNew; otherwise, use Truncate.
|
|
// Specifies that the operating system should create a new file.
|
|
// If the file already exists, it will be overwritten.
|
|
IFileSystem? fs = GetFirstWhereExists(path);
|
|
if (fs != null)
|
|
return fs.OpenFile(path, FileMode.Truncate, access);
|
|
|
|
return GetFirstWritable().CreateFile(path);
|
|
}
|
|
|
|
case FileMode.Truncate:
|
|
{
|
|
// - Requires Write permission.
|
|
// - Requires no Read access requested
|
|
// - Requires existing file
|
|
// When the file is opened, it should be truncated so that its size is zero bytes.
|
|
|
|
IFileSystem? fs = GetFirstWhereExists(path);
|
|
if (fs == null)
|
|
throw new FileNotFoundException($"Could not find the file at the specified path: {path}", nameof(path));
|
|
|
|
return fs.OpenFile(path, FileMode.Truncate, access);
|
|
}
|
|
case FileMode.Append:
|
|
{
|
|
// Opens the file if it exists and seeks to the end of the file, or creates a new file.
|
|
// This requires R/W permission.
|
|
// Trying to seek to a position before the end of the file throws an IOException exception, and any attempt to read fails and throws a NotSupportedException exception.
|
|
|
|
IFileSystem? fs = GetFirstWhereExists(path);
|
|
if (fs == null)
|
|
throw new FileNotFoundException($"Could not find the file at the specified path: {path}", nameof(path));
|
|
|
|
return fs.OpenFile(path, FileMode.Append, access);
|
|
}
|
|
|
|
default:
|
|
throw new ArgumentOutOfRangeException(nameof(mode), mode, null);
|
|
}
|
|
}
|
|
|
|
public void CreateDirectory(FileSystemPath path)
|
|
{
|
|
if (Exists(path))
|
|
throw new ArgumentException("The specified directory already exists.");
|
|
IFileSystem? fs = GetFirstWhereExists(path.ParentPath);
|
|
if (fs == null)
|
|
throw new ArgumentException("The directory-parent does not exist.");
|
|
fs.CreateDirectory(path);
|
|
}
|
|
|
|
public void Delete(FileSystemPath path)
|
|
{
|
|
foreach (var fs in FileSystems.Where(fs => fs.Exists(path)))
|
|
fs.Delete(path);
|
|
}
|
|
}
|