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..
This commit is contained in:
duckdoom4
2023-09-08 22:53:30 +02:00
parent 39308b2509
commit e74bfbc365
11 changed files with 310 additions and 157 deletions

View File

@@ -111,13 +111,13 @@ public bool Exists(FileSystemPath path)
public Stream CreateFile(FileSystemPath path)
{
var zae = ZipArchive.CreateEntry(ToEntryPath(path));
return zae.Open();
ZipArchiveEntry entry = ZipArchive.CreateEntry(ToEntryPath(path));
return entry.Open();
}
public Stream OpenFile(FileSystemPath path, FileAccess access)
public Stream OpenFile(FileSystemPath path, FileMode mode, FileAccess access)
{
var entry = ZipArchive.GetEntry(ToEntryPath(path));
ZipArchiveEntry? entry = ZipArchive.GetEntry(ToEntryPath(path));
return entry?.Open() ?? Stream.Null;
}
@@ -128,7 +128,7 @@ public void CreateDirectory(FileSystemPath path)
public void Delete(FileSystemPath path)
{
var entry = ZipArchive.GetEntry(ToEntryPath(path));
ZipArchiveEntry? entry = ZipArchive.GetEntry(ToEntryPath(path));
entry?.Delete();
}
}

View File

@@ -7,36 +7,109 @@ namespace pkNX.Containers.VFS;
public interface IFileSystem : IDisposable
{
bool IsReadOnly => false;
IEnumerable<FileSystemPath> GetEntityPaths(FileSystemPath path, Func<FileSystemPath, bool>? filter = null);
IEnumerable<FileSystemPath> GetDirectoryPaths(FileSystemPath path, Func<FileSystemPath, bool>? filter = null);
IEnumerable<FileSystemPath> GetFilePaths(FileSystemPath path, Func<FileSystemPath, bool>? filter = null);
/// <summary>
/// Checks if the specified path exists in the filesystem.
/// </summary>
/// <param name="path">The path to check.</param>
/// <returns>True if the path exists, false otherwise.</returns>
bool Exists(FileSystemPath path);
Stream CreateFile(FileSystemPath path);
Stream OpenFile(FileSystemPath path, FileAccess access);
/// <summary>
/// Opens a stream to the location of the specified file with the specified mode and access.
/// </summary>
/// <param name="path">The path and name of the file to open.</param>
/// <param name="mode">A FileMode value that specifies whether a file is created if one does not exist, and determines whether the contents of existing files are retained or overwritten.</param>
/// <param name="access">The access mode to use when opening the file.</param>
/// <returns>A stream to the location of the opened file.</returns>
Stream OpenFile(FileSystemPath path, FileMode mode = FileMode.Open, FileAccess access = FileAccess.Read);
/// <summary>
/// Creates or overwrites a file in the specified path
/// </summary>
/// <param name="path">The path and name of the file to create.</param>
/// <returns>A stream to the location of the new file.</returns>
public Stream CreateFile(FileSystemPath path) => OpenFile(path, FileMode.Create, FileAccess.Write);
/// <summary>
/// Opens a stream to the location of the specified file using FileMode.OpenOrCreate and FileAccess.Write.
/// </summary>
/// <param name="path">The path and name of the file to open.</param>
/// <returns>A stream to the location of the opened file.</returns>
public Stream OpenWrite(FileSystemPath path) => OpenFile(path, FileMode.OpenOrCreate, FileAccess.Write);
/// <summary>
/// Creates a directory in the specified path.
/// </summary>
/// <param name="path">The path and name of the directory to create.</param>
void CreateDirectory(FileSystemPath path);
/// <summary>
/// Deletes the specified file or directory. Does not throw an exception if the specified file or directory does not exist.
/// </summary>
/// <param name="path">The path and name of the file or directory to delete.</param>
void Delete(FileSystemPath path);
bool IsReadOnly => false;
public void Move(FileSystemPath sourcePath, IFileSystem destinationFileSystem, FileSystemPath destinationPath)
{
throw new NotImplementedException();
}
public void Copy(FileSystemPath sourcePath, IFileSystem destinationFileSystem, FileSystemPath destinationPath)
{
throw new NotImplementedException();
}
public IEnumerable<FileSystemPath> GetEntitiesRecursive(FileSystemPath path, Func<FileSystemPath, bool>? filter = null)
{
if (!path.IsDirectory)
throw new ArgumentException("The specified path is not a directory.");
foreach (var entity in GetEntityPaths(path, filter))
{
yield return entity;
if (!entity.IsDirectory)
continue;
foreach (var subEntity in GetEntitiesRecursive(entity, filter))
yield return subEntity;
}
}
public void CreateDirectoryRecursive(FileSystemPath path)
{
if (!path.IsDirectory)
throw new ArgumentException("The specified path is not a directory.");
var currentDirectoryPath = FileSystemPath.Root;
foreach (var dirName in path.GetDirectorySegments())
{
currentDirectoryPath = currentDirectoryPath.AppendDirectory(dirName);
if (!Exists(currentDirectoryPath))
CreateDirectory(currentDirectoryPath);
}
}
public string ReadAllText(FileSystemPath path)
{
if (!Exists(path))
return string.Empty;
using var stream = OpenFile(path, FileAccess.Read);
using var stream = OpenFile(path);
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
public void WriteAllText(FileSystemPath path, string content)
{
if (!Exists(path))
{
CreateFile(path);
}
using var stream = OpenFile(path, FileAccess.Write);
using var stream = OpenWrite(path);
using var writer = new StreamWriter(stream);
writer.Write(content);
}
@@ -53,16 +126,16 @@ public static RelativeFileSystem AsRelativeFileSystem(this IFileSystem self, Pat
public static IEnumerable<IFileSystemEntity> GetEntities(this IFileSystem self, FileSystemPath path, Func<FileSystemPath, bool>? filter = null)
{
return self.GetEntityPaths(path, filter).Select(p => IFileSystemEntity.Create(self, p));
return self.GetEntityPaths(path, filter).Select(p => IFileSystemEntity.Create(self, p)).OrderBy(x => x.Path);
}
public static IEnumerable<VirtualDirectory> GetDirectories(this IFileSystem self, FileSystemPath path, Func<FileSystemPath, bool>? filter = null)
{
return self.GetDirectoryPaths(path, filter).Select(p => VirtualDirectory.Create(self, p));
return self.GetDirectoryPaths(path, filter).Select(p => VirtualDirectory.Create(self, p)).OrderBy(x => x.Path);
}
public static IEnumerable<VirtualFile> GetFiles(this IFileSystem self, FileSystemPath path, Func<FileSystemPath, bool>? filter = null)
{
return self.GetFilePaths(path, filter).Select(p => VirtualFile.Create(self, p));
return self.GetFilePaths(path, filter).Select(p => VirtualFile.Create(self, p)).OrderBy(x => x.Path);
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
@@ -9,15 +10,18 @@ namespace pkNX.Containers.VFS;
public class LayeredFileSystem : IFileSystem
{
public IEnumerable<IFileSystem> FileSystems { get; }
public IReadOnlyList<IFileSystem> FileSystems { get; }
public LayeredFileSystem(IEnumerable<IFileSystem> fileSystems)
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.AsEnumerable())
this(fileSystems.AsReadOnly())
{ }
public void Dispose()
@@ -52,35 +56,157 @@ public IEnumerable<FileSystemPath> GetFilePaths(FileSystemPath path, Func<FileSy
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? GetFirst(FileSystemPath path)
public IFileSystem? GetFirstWhereExists(FileSystemPath path)
{
return FileSystems.FirstOrDefault(fs => fs.Exists(path));
}
public Stream CreateFile(FileSystemPath path)
public IFileSystem? GetFirstWritableWhereExists(FileSystemPath path)
{
IFileSystem fs = GetFirst(path) ?? FileSystems.First();
return fs.CreateFile(path);
return FileSystems.FirstOrDefault(fs => !fs.IsReadOnly && fs.Exists(path));
}
public Stream OpenFile(FileSystemPath path, FileAccess access)
private bool ValidateOpenMode(FileMode mode = FileMode.Open, FileAccess access = FileAccess.Read)
{
IFileSystem? fs = GetFirst(path);
if (fs == null)
throw new FileNotFoundException($"Unable to find {path.Path}");
return fs.OpenFile(path, access);
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 = GetFirst(path.ParentPath);
IFileSystem? fs = GetFirstWhereExists(path.ParentPath);
if (fs == null)
throw new ArgumentException("The directory-parent does not exist.");
fs.CreateDirectory(path);

View File

@@ -88,11 +88,11 @@ public Stream CreateFile(FileSystemPath path)
return File.Create(GetPhysicalPath(path));
}
public Stream OpenFile(FileSystemPath path, FileAccess access)
public Stream OpenFile(FileSystemPath path, FileMode mode = FileMode.Open, FileAccess access = FileAccess.Read)
{
if (!path.IsFile)
throw new ArgumentException("The specified path is not a file.", nameof(path));
return File.Open(GetPhysicalPath(path), FileMode.Open, access);
return File.Open(GetPhysicalPath(path), mode, access);
}
public void CreateDirectory(FileSystemPath path)

View File

@@ -47,25 +47,28 @@ public bool Exists(FileSystemPath path)
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Stream OpenFile(FileSystemPath path, FileAccess access)
public Stream OpenFile(FileSystemPath path, FileMode mode = FileMode.Open, FileAccess access = FileAccess.Read)
{
if (access != FileAccess.Read)
throw new InvalidOperationException("This is a read-only filesystem.");
return FileSystem.OpenFile(path, access);
throw new UnauthorizedAccessException("This is a read-only filesystem.");
return FileSystem.OpenFile(path, mode, access);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Stream CreateFile(FileSystemPath path)
{
throw new InvalidOperationException("This is a read-only filesystem.");
throw new UnauthorizedAccessException("This is a read-only filesystem.");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void CreateDirectory(FileSystemPath path)
{
throw new InvalidOperationException("This is a read-only filesystem.");
throw new UnauthorizedAccessException("This is a read-only filesystem.");
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Delete(FileSystemPath path)
{
throw new InvalidOperationException("This is a read-only filesystem.");
throw new UnauthorizedAccessException("This is a read-only filesystem.");
}
}

View File

@@ -58,9 +58,9 @@ public bool Exists(FileSystemPath path)
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Stream OpenFile(FileSystemPath path, FileAccess access)
public Stream OpenFile(FileSystemPath path, FileMode mode = FileMode.Open, FileAccess access = FileAccess.Read)
{
return FileSystem.OpenFile(ToAbsolutePath(path), access);
return FileSystem.OpenFile(ToAbsolutePath(path), mode, access);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]

View File

@@ -1,92 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
namespace pkNX.Containers.VFS;
public static class FileSystemExtensions
{
public static IEnumerable<FileSystemPath> GetEntityPaths(this VirtualDirectory directory, Func<FileSystemPath, bool>? filter = null)
{
return directory.FileSystem.GetEntityPaths(directory.Path, filter);
}
public static IEnumerable<FileSystemPath> GetEntitiesRecursive(this IFileSystem fileSystem, FileSystemPath path, Func<FileSystemPath, bool>? filter = null)
{
if (!path.IsDirectory)
throw new ArgumentException("The specified path is not a directory.");
foreach (var entity in fileSystem.GetEntityPaths(path, filter))
{
yield return entity;
if (!entity.IsDirectory)
continue;
foreach (var subEntity in fileSystem.GetEntitiesRecursive(entity, filter))
yield return subEntity;
}
}
public static void CreateDirectoryRecursive(this IFileSystem fileSystem, FileSystemPath path)
{
if (!path.IsDirectory)
throw new ArgumentException("The specified path is not a directory.");
var currentDirectoryPath = FileSystemPath.Root;
foreach (var dirName in path.GetDirectorySegments())
{
currentDirectoryPath = currentDirectoryPath.AppendDirectory(dirName);
if (!fileSystem.Exists(currentDirectoryPath))
fileSystem.CreateDirectory(currentDirectoryPath);
}
}
/*#region Move Extensions
public static void Move(this IFileSystem sourceFileSystem, FileSystemPath sourcePath, IFileSystem destinationFileSystem, FileSystemPath destinationPath)
{
if (!EntityMovers.Registration.TryGetSupported(sourceFileSystem.GetType(), destinationFileSystem.GetType(), out IEntityMover mover))
throw new ArgumentException("The specified combination of file-systems is not supported.");
mover.Move(sourceFileSystem, sourcePath, destinationFileSystem, destinationPath);
}
public static void MoveTo(this FileSystemEntity entity, IFileSystem destinationFileSystem, FileSystemPath destinationPath)
{
entity.FileSystem.Move(entity.Path, destinationFileSystem, destinationPath);
}
public static void MoveTo(this VirtualDirectory source, VirtualDirectory destination)
{
source.FileSystem.Move(source.Path, destination.FileSystem, destination.Path.AppendDirectory(source.Path.EntityName));
}
public static void MoveTo(this VirtualFile source, VirtualDirectory destination)
{
source.FileSystem.Move(source.Path, destination.FileSystem, destination.Path.AppendFile(source.Path.EntityName));
}
#endregion
#region Copy Extensions
public static void Copy(this IFileSystem sourceFileSystem, FileSystemPath sourcePath, IFileSystem destinationFileSystem, FileSystemPath destinationPath)
{
if (!EntityCopiers.Registration.TryGetSupported(sourceFileSystem.GetType(), destinationFileSystem.GetType(), out IEntityCopier copier))
throw new ArgumentException("The specified combination of file-systems is not supported.");
copier.Copy(sourceFileSystem, sourcePath, destinationFileSystem, destinationPath);
}
public static void CopyTo(this FileSystemEntity entity, IFileSystem destinationFileSystem, FileSystemPath destinationPath)
{
entity.FileSystem.Copy(entity.Path, destinationFileSystem, destinationPath);
}
public static void CopyTo(this VirtualDirectory source, VirtualDirectory destination)
{
source.FileSystem.Copy(source.Path, destination.FileSystem, destination.Path.AppendDirectory(source.Path.EntityName));
}
public static void CopyTo(this VirtualFile source, VirtualDirectory destination)
{
source.FileSystem.Copy(source.Path, destination.FileSystem, destination.Path.AppendFile(source.Path.EntityName));
}
#endregion*/
}

View File

@@ -7,6 +7,26 @@ public interface IFileSystemEntity
string Name { get; }
VirtualDirectory ParentDirectory { get; }
public void CopyTo(IFileSystem destinationFileSystem, FileSystemPath destinationPath)
{
FileSystem.Copy(Path, destinationFileSystem, destinationPath);
}
public void MoveTo(IFileSystem destinationFileSystem, FileSystemPath destinationPath)
{
FileSystem.Move(Path, destinationFileSystem, destinationPath);
}
public void Delete()
{
FileSystem.Delete(Path);
}
public void Exists()
{
FileSystem.Exists(Path);
}
internal static IFileSystemEntity Create(IFileSystem fileSystem, FileSystemPath path)
{
if (path.IsFile)
@@ -16,16 +36,3 @@ internal static IFileSystemEntity Create(IFileSystem fileSystem, FileSystemPath
}
}
public static class IFileSystemEntityExtensions
{
public static void Delete(this IFileSystemEntity e)
{
e.FileSystem.Delete(e.Path);
}
public static void Exists(this IFileSystemEntity e)
{
e.FileSystem.Exists(e.Path);
}
}

View File

@@ -8,6 +8,15 @@ namespace pkNX.Containers.VFS;
public string Name => Path.EntityName;
public VirtualDirectory ParentDirectory => Create(FileSystem, Path.ParentPath);
public void CopyTo(VirtualDirectory destination)
{
FileSystem.Copy(Path, destination.FileSystem, destination.Path.AppendDirectory(Name));
}
public void MoveTo(VirtualDirectory destination)
{
FileSystem.Move(Path, destination.FileSystem, destination.Path.AppendDirectory(Name));
}
internal static VirtualDirectory Create(IFileSystem fileSystem, FileSystemPath path)
{
@@ -16,4 +25,9 @@ internal static VirtualDirectory Create(IFileSystem fileSystem, FileSystemPath p
return new VirtualDirectory(fileSystem, path);
}
public IEnumerable<FileSystemPath> GetEntityPaths(Func<FileSystemPath, bool>? filter = null)
{
return FileSystem.GetEntityPaths(Path, filter);
}
}

View File

@@ -26,9 +26,29 @@ public string GetExtension()
return Name[(lastPeriod + 1)..];
}
public Stream Open(FileAccess access = FileAccess.Read)
public Stream Open(FileMode mode = FileMode.Open, FileAccess access = FileAccess.Read)
{
return FileSystem.OpenFile(Path, access);
return FileSystem.OpenFile(Path, mode, access);
}
public Stream OpenRead()
{
return FileSystem.OpenFile(Path, FileMode.Open, FileAccess.Read);
}
public Stream OpenWrite()
{
return FileSystem.OpenWrite(Path);
}
public void CopyTo(VirtualDirectory destination)
{
FileSystem.Copy(Path, destination.FileSystem, destination.Path.AppendFile(Name));
}
public void MoveTo(VirtualDirectory destination)
{
FileSystem.Move(Path, destination.FileSystem, destination.Path.AppendFile(Name));
}
public ReadOnlySpan<byte> ReadAllBytes()
@@ -57,13 +77,13 @@ public string ReadAllText()
public void WriteAllBytes(ReadOnlySpan<byte> bytes)
{
using var stream = Open(FileAccess.Write);
using var stream = OpenWrite();
stream.Write(bytes);
}
public void WriteAllText(string text)
{
using var stream = Open(FileAccess.Write);
using var stream = OpenWrite();
using var writer = new StreamWriter(stream);
writer.Write(text);
}

View File

@@ -30,12 +30,15 @@ public MountPoint(FileSystemPath mountPath, IFileSystem fileSystem)
public class VirtualFileSystem : IFileSystem
{
public static VirtualFileSystem Current { get; private set; } = null!;
public SortedSet<MountPoint> Mounts { get; }
public bool IsReadOnly => Mounts.All(x => x.FileSystem.IsReadOnly);
public VirtualFileSystem(IEnumerable<MountPoint> mounts)
{
Mounts = new SortedSet<MountPoint>(mounts);
Current = this;
}
public VirtualFileSystem(params MountPoint[] mounts) :
@@ -84,18 +87,17 @@ public bool Exists(FileSystemPath path)
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Stream CreateFile(FileSystemPath path)
public Stream OpenFile(FileSystemPath path, FileMode mode = FileMode.Open, FileAccess access = FileAccess.Read)
{
var mount = GetMountPoint(path);
return mount.FileSystem.CreateFile(path);
return mount.FileSystem.OpenFile(path, mode, access);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Stream OpenFile(FileSystemPath path, FileAccess access)
{
var mount = GetMountPoint(path);
return mount.FileSystem.OpenFile(path, access);
}
public Stream OpenWrite(FileSystemPath path) => OpenFile(path, FileMode.OpenOrCreate, FileAccess.Write);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Stream CreateFile(FileSystemPath path) => OpenFile(path, FileMode.Create, FileAccess.Write);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void CreateDirectory(FileSystemPath path)