pkNX/pkNX.Containers/VFS/FileSystems/LayeredFileSystem.cs
duckdoom4 e74bfbc365 Made VFS fully functional
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..
2023-09-08 22:58:26 +02:00

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);
}
}