Made unreal work and make the fake drive update with the json so you can edit the fake config and it writes to the real drive.

This commit is contained in:
2026-01-07 09:16:57 +11:00
parent 9070768fe7
commit dafeeb0f15
9 changed files with 919 additions and 1387 deletions

57
DevDriveConfig.cs Normal file
View File

@@ -0,0 +1,57 @@
using System.Text.Json.Serialization;
namespace DevDrive;
/// <summary>
/// Represents the index.json configuration file structure.
/// </summary>
public class DevDriveConfig
{
/// <summary>
/// Display name for the drive.
/// </summary>
[JsonPropertyName("driveName")]
public string DriveName { get; set; } = "Dev Drive";
/// <summary>
/// Icon path in format "path,index" (e.g. "%SystemRoot%\System32\shell32.dll,8").
/// </summary>
[JsonPropertyName("driveIcon")]
public string DriveIcon { get; set; } = "";
/// <summary>
/// Total drive capacity in MB (for display purposes).
/// </summary>
[JsonPropertyName("driveTotalMB")]
public double DriveTotalMB { get; set; } = 1.44;
/// <summary>
/// Free space in MB (for display purposes).
/// </summary>
[JsonPropertyName("driveFreeMB")]
public double DriveFreeMB { get; set; } = 0.72;
/// <summary>
/// List of symlink entries to create in the drive root.
/// </summary>
[JsonPropertyName("paths")]
public List<SymlinkEntry> Paths { get; set; } = [];
}
/// <summary>
/// Represents a single symlink entry.
/// </summary>
public class SymlinkEntry
{
/// <summary>
/// Name of the symlink (folder/file name in the drive).
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Target path the symlink points to.
/// </summary>
[JsonPropertyName("path")]
public string Path { get; set; } = string.Empty;
}

View File

@@ -1,105 +0,0 @@
using System.Runtime.Versioning;
using Microsoft.Win32;
namespace DevDrive;
/// <summary>
/// Sets custom drive icons via the Windows registry.
/// </summary>
[SupportedOSPlatform("windows")]
public static class DriveIconHelper
{
private const string LogPrefix = "[DriveIcon]";
private const string DriveIconsKey = @"Software\Classes\Applications\Explorer.exe\Drives";
private static string? _currentDriveLetter;
public static bool SetDriveIcon(string driveLetter, string iconPath, string? driveLabel = null)
{
try
{
var letter = driveLetter.TrimEnd('\\', ':').ToUpperInvariant();
if (letter.Length != 1 || letter[0] < 'A' || letter[0] > 'Z')
{
Console.WriteLine($"{LogPrefix} Invalid drive letter: {driveLetter}");
return false;
}
var driveKeyPath = @$"{DriveIconsKey}\{letter}\DefaultIcon";
using var key = Registry.CurrentUser.CreateSubKey(driveKeyPath);
if (key == null)
{
Console.WriteLine($"{LogPrefix} Failed to create registry key");
return false;
}
key.SetValue("", iconPath);
_currentDriveLetter = letter;
if (!string.IsNullOrEmpty(driveLabel))
{
var labelKeyPath = @$"{DriveIconsKey}\{letter}\DefaultLabel";
using var labelKey = Registry.CurrentUser.CreateSubKey(labelKeyPath);
labelKey?.SetValue("", driveLabel);
}
RefreshShell();
Console.WriteLine($"{LogPrefix} Set icon for drive {letter}: to {iconPath}");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error setting icon: {ex.Message}");
return false;
}
}
public static void RemoveDriveIcon()
{
if (_currentDriveLetter == null)
{
return;
}
try
{
var driveKeyPath = @$"{DriveIconsKey}\{_currentDriveLetter}";
Registry.CurrentUser.DeleteSubKeyTree(driveKeyPath, throwOnMissingSubKey: false);
Console.WriteLine($"{LogPrefix} Removed icon for drive {_currentDriveLetter}:");
_currentDriveLetter = null;
RefreshShell();
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error removing icon: {ex.Message}");
}
}
private static void RefreshShell()
{
try
{
NativeMethods.SHChangeNotify(
NativeMethods.SHCNE_ASSOCCHANGED,
NativeMethods.SHCNF_IDLIST,
IntPtr.Zero,
IntPtr.Zero);
}
catch
{
}
}
private static class NativeMethods
{
public const int SHCNE_ASSOCCHANGED = 0x08000000;
public const int SHCNF_IDLIST = 0x0000;
[System.Runtime.InteropServices.DllImport("shell32.dll")]
public static extern void SHChangeNotify(int wEventId, int uFlags, IntPtr dwItem1, IntPtr dwItem2);
}
}

View File

@@ -1,33 +0,0 @@
using System.Text.Json.Serialization;
namespace DevDrive;
/// <summary>
/// Represents the index.json configuration file structure.
/// </summary>
public class FloppyConfig
{
[JsonPropertyName("driveName")]
public string DriveName { get; set; } = "Dev Disk";
[JsonPropertyName("driveIcon")]
public string DriveIcon { get; set; } = "";
[JsonPropertyName("driveTotalMB")]
public double DriveTotalMB { get; set; } = 100;
[JsonPropertyName("driveFreeMB")]
public double DriveFreeMB { get; set; } = 50;
[JsonPropertyName("paths")]
public List<SymlinkEntry> Paths { get; set; } = [];
}
public class SymlinkEntry
{
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("path")]
public string Path { get; set; } = string.Empty;
}

View File

@@ -1,39 +0,0 @@
using System.Security.AccessControl;
namespace DevDrive;
/// <summary>
/// Represents a file or directory node in the virtual file system.
/// </summary>
public class FloppyFileNode
{
public string Name { get; set; }
public string FullPath { get; set; }
public bool IsDirectory { get; set; }
public FileAttributes Attributes { get; set; }
public byte[] Data { get; set; }
public DateTime CreationTime { get; set; }
public DateTime LastAccessTime { get; set; }
public DateTime LastWriteTime { get; set; }
public FileSystemSecurity? Security { get; set; }
public string? SymlinkTarget { get; set; }
public bool IsSymlink => !string.IsNullOrEmpty(SymlinkTarget);
public FloppyFileNode(string fullPath, bool isDirectory)
{
FullPath = fullPath.TrimEnd('\\');
Name = Path.GetFileName(fullPath);
if (string.IsNullOrEmpty(Name))
{
Name = "\\";
}
IsDirectory = isDirectory;
Attributes = isDirectory ? FileAttributes.Directory : FileAttributes.Normal;
Data = [];
CreationTime = DateTime.Now;
LastAccessTime = DateTime.Now;
LastWriteTime = DateTime.Now;
}
public long FileSize => IsDirectory ? 0 : Data.Length;
}

View File

@@ -1,631 +0,0 @@
using System.Security.AccessControl;
using DokanNet;
using FileAccess = DokanNet.FileAccess;
namespace DevDrive;
public class FloppyFileSystem : IDokanOperations
{
private const string DefaultVolumeName = "Dev Disk";
private const string FileSystemName = "FAT";
private const uint VolumeSerial = 0x19950101;
private readonly FloppyStorage _storage;
private readonly string _volumeName;
public FloppyFileSystem(string? volumeName = null, long totalBytes = FloppyStorage.DefaultCapacity, long freeBytes = FloppyStorage.DefaultCapacity)
{
_storage = new FloppyStorage(totalBytes, freeBytes);
_volumeName = volumeName ?? DefaultVolumeName;
}
public FloppyFileSystem(Dictionary<string, string>? symlinks, string? volumeName, long totalBytes, long freeBytes)
{
_storage = new FloppyStorage(totalBytes, freeBytes);
_volumeName = volumeName ?? DefaultVolumeName;
if (symlinks != null)
{
foreach (var (path, target) in symlinks)
{
_storage.CreateSymlink(path, target, true);
}
}
}
public NtStatus CreateFile(
string fileName,
FileAccess access,
FileShare share,
FileMode mode,
FileOptions options,
FileAttributes attributes,
IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
if (SymlinkHandler.TryResolveSymlink(path, _storage, out var realPath) && realPath != null)
{
return SymlinkHandler.CreateRealFileOrDirectory(realPath, info.IsDirectory, mode);
}
var node = _storage.GetNode(path);
var parentPath = FloppyStorage.GetParentPath(path);
var parentExists = _storage.Exists(parentPath);
if (info.IsDirectory)
{
switch (mode)
{
case FileMode.CreateNew:
if (node != null)
{
return DokanResult.FileExists;
}
if (!parentExists)
{
return DokanResult.PathNotFound;
}
var newDir = _storage.CreateNode(path, true);
if (newDir == null)
{
return DokanResult.Error;
}
return DokanResult.Success;
case FileMode.Open:
if (node == null)
{
return DokanResult.PathNotFound;
}
if (!node.IsDirectory)
{
return DokanResult.NotADirectory;
}
return DokanResult.Success;
default:
return DokanResult.Success;
}
}
var pathExists = node != null;
var pathIsDirectory = node?.IsDirectory ?? false;
switch (mode)
{
case FileMode.Open:
if (!pathExists)
{
return DokanResult.FileNotFound;
}
if (pathIsDirectory)
{
info.IsDirectory = true;
}
break;
case FileMode.CreateNew:
if (pathExists)
{
return DokanResult.FileExists;
}
if (!parentExists)
{
return DokanResult.PathNotFound;
}
_storage.CreateNode(path, false);
break;
case FileMode.Create:
if (pathExists && pathIsDirectory)
{
return DokanResult.AccessDenied;
}
if (!parentExists)
{
return DokanResult.PathNotFound;
}
if (pathExists)
{
var existing = _storage.GetNode(path);
if (existing != null)
{
existing.Data = [];
existing.LastWriteTime = DateTime.Now;
}
}
else
{
_storage.CreateNode(path, false);
}
break;
case FileMode.OpenOrCreate:
if (!pathExists)
{
if (!parentExists)
{
return DokanResult.PathNotFound;
}
_storage.CreateNode(path, false);
}
else if (pathIsDirectory)
{
info.IsDirectory = true;
}
break;
case FileMode.Truncate:
if (!pathExists)
{
return DokanResult.FileNotFound;
}
if (pathIsDirectory)
{
return DokanResult.AccessDenied;
}
var truncNode = _storage.GetNode(path);
if (truncNode != null)
{
truncNode.Data = [];
truncNode.LastWriteTime = DateTime.Now;
}
break;
case FileMode.Append:
if (!pathExists)
{
if (!parentExists)
{
return DokanResult.PathNotFound;
}
_storage.CreateNode(path, false);
}
break;
}
return DokanResult.Success;
}
public void Cleanup(string fileName, IDokanFileInfo info)
{
if (info.IsDirectory && _storage.Exists(FloppyStorage.NormalizePath(fileName)))
{
return;
}
}
public void CloseFile(string fileName, IDokanFileInfo info)
{
}
public NtStatus ReadFile(
string fileName,
byte[] buffer,
out int bytesRead,
long offset,
IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
if (SymlinkHandler.TryResolveSymlink(path, _storage, out var realPath) && realPath != null)
{
return SymlinkHandler.ReadRealFile(realPath, buffer, out bytesRead, offset);
}
var node = _storage.GetNode(path);
if (node == null || node.IsDirectory)
{
bytesRead = 0;
return DokanResult.FileNotFound;
}
node.LastAccessTime = DateTime.Now;
if (offset >= node.Data.Length)
{
bytesRead = 0;
return DokanResult.Success;
}
var toRead = Math.Min(buffer.Length, (int)(node.Data.Length - offset));
Array.Copy(node.Data, offset, buffer, 0, toRead);
bytesRead = toRead;
return DokanResult.Success;
}
public NtStatus WriteFile(
string fileName,
byte[] buffer,
out int bytesWritten,
long offset,
IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
if (SymlinkHandler.TryResolveSymlink(path, _storage, out var realPath) && realPath != null)
{
return SymlinkHandler.WriteRealFile(realPath, buffer, out bytesWritten, offset);
}
var node = _storage.GetNode(path);
if (node == null || node.IsDirectory)
{
bytesWritten = 0;
return DokanResult.FileNotFound;
}
if (info.PagingIo)
{
if (offset >= node.Data.Length)
{
bytesWritten = 0;
return DokanResult.Success;
}
var toWrite = Math.Min(buffer.Length, (int)(node.Data.Length - offset));
Array.Copy(buffer, 0, node.Data, offset, toWrite);
bytesWritten = toWrite;
return DokanResult.Success;
}
var newLength = offset + buffer.Length;
var additionalBytes = newLength - node.Data.Length;
if (additionalBytes > 0 && !_storage.HasEnoughSpace(additionalBytes))
{
bytesWritten = 0;
return DokanResult.DiskFull;
}
if (newLength > node.Data.Length)
{
var newData = new byte[newLength];
Array.Copy(node.Data, newData, node.Data.Length);
node.Data = newData;
}
Array.Copy(buffer, 0, node.Data, offset, buffer.Length);
bytesWritten = buffer.Length;
node.LastWriteTime = DateTime.Now;
return DokanResult.Success;
}
public NtStatus FlushFileBuffers(string fileName, IDokanFileInfo info)
{
return DokanResult.Success;
}
public NtStatus GetFileInformation(
string fileName,
out FileInformation fileInfo,
IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
if (SymlinkHandler.TryResolveSymlink(path, _storage, out var realPath) && realPath != null)
{
if (SymlinkHandler.GetRealFileInfo(realPath, out fileInfo))
{
return DokanResult.Success;
}
return DokanResult.FileNotFound;
}
var node = _storage.GetNode(path);
if (node == null)
{
fileInfo = default;
return DokanResult.FileNotFound;
}
fileInfo = new FileInformation
{
FileName = node.Name,
Attributes = node.Attributes,
CreationTime = node.CreationTime,
LastAccessTime = node.LastAccessTime,
LastWriteTime = node.LastWriteTime,
Length = node.FileSize
};
return DokanResult.Success;
}
public NtStatus FindFiles(
string fileName,
out IList<FileInformation> files,
IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
if (SymlinkHandler.TryResolveSymlink(path, _storage, out var realPath) && realPath != null)
{
files = SymlinkHandler.ListRealDirectory(realPath);
return DokanResult.Success;
}
var children = _storage.GetChildren(path);
files = children.Select(node => new FileInformation
{
FileName = node.Name,
Attributes = node.Attributes,
CreationTime = node.CreationTime,
LastAccessTime = node.LastAccessTime,
LastWriteTime = node.LastWriteTime,
Length = node.FileSize
}).ToList();
return DokanResult.Success;
}
public NtStatus FindFilesWithPattern(
string fileName,
string searchPattern,
out IList<FileInformation> files,
IDokanFileInfo info)
{
files = [];
return DokanResult.NotImplemented;
}
public NtStatus SetFileAttributes(
string fileName,
FileAttributes attributes,
IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
var node = _storage.GetNode(path);
if (node == null)
{
return DokanResult.FileNotFound;
}
node.Attributes = attributes;
return DokanResult.Success;
}
public NtStatus SetFileTime(
string fileName,
DateTime? creationTime,
DateTime? lastAccessTime,
DateTime? lastWriteTime,
IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
var node = _storage.GetNode(path);
if (node == null)
{
return DokanResult.FileNotFound;
}
if (creationTime.HasValue)
{
node.CreationTime = creationTime.Value;
}
if (lastAccessTime.HasValue)
{
node.LastAccessTime = lastAccessTime.Value;
}
if (lastWriteTime.HasValue)
{
node.LastWriteTime = lastWriteTime.Value;
}
return DokanResult.Success;
}
public NtStatus DeleteFile(string fileName, IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
var node = _storage.GetNode(path);
if (node == null)
{
return DokanResult.FileNotFound;
}
if (node.IsDirectory)
{
return DokanResult.AccessDenied;
}
return DokanResult.Success;
}
public NtStatus DeleteDirectory(string fileName, IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
var node = _storage.GetNode(path);
if (node == null)
{
return DokanResult.PathNotFound;
}
if (!node.IsDirectory)
{
return DokanResult.NotADirectory;
}
var children = _storage.GetChildren(path);
if (children.Any())
{
return DokanResult.DirectoryNotEmpty;
}
return DokanResult.Success;
}
public NtStatus MoveFile(
string oldName,
string newName,
bool replace,
IDokanFileInfo info)
{
var success = _storage.MoveNode(oldName, newName, replace);
return success ? DokanResult.Success : DokanResult.FileNotFound;
}
public NtStatus SetEndOfFile(string fileName, long length, IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
var node = _storage.GetNode(path);
if (node == null || node.IsDirectory)
{
return DokanResult.FileNotFound;
}
var additionalBytes = length - node.Data.Length;
if (additionalBytes > 0 && !_storage.HasEnoughSpace(additionalBytes))
{
return DokanResult.DiskFull;
}
if (length != node.Data.Length)
{
var newData = new byte[length];
var copyLength = Math.Min(length, node.Data.Length);
Array.Copy(node.Data, newData, copyLength);
node.Data = newData;
node.LastWriteTime = DateTime.Now;
}
return DokanResult.Success;
}
public NtStatus SetAllocationSize(string fileName, long length, IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
var node = _storage.GetNode(path);
if (node == null || node.IsDirectory)
{
return DokanResult.FileNotFound;
}
if (length < node.Data.Length)
{
var newData = new byte[length];
Array.Copy(node.Data, newData, length);
node.Data = newData;
node.LastWriteTime = DateTime.Now;
}
return DokanResult.Success;
}
public NtStatus LockFile(
string fileName,
long offset,
long length,
IDokanFileInfo info)
{
return DokanResult.Success;
}
public NtStatus UnlockFile(
string fileName,
long offset,
long length,
IDokanFileInfo info)
{
return DokanResult.Success;
}
public NtStatus GetDiskFreeSpace(
out long freeBytesAvailable,
out long totalNumberOfBytes,
out long totalNumberOfFreeBytes,
IDokanFileInfo info)
{
freeBytesAvailable = _storage.FreeSpace;
totalNumberOfBytes = _storage.Capacity;
totalNumberOfFreeBytes = _storage.FreeSpace;
return DokanResult.Success;
}
public NtStatus GetVolumeInformation(
out string volumeLabel,
out FileSystemFeatures features,
out string fileSystemName,
out uint maximumComponentLength,
IDokanFileInfo info)
{
volumeLabel = _volumeName;
fileSystemName = FileSystemName;
maximumComponentLength = 255;
features = FileSystemFeatures.CasePreservedNames
| FileSystemFeatures.UnicodeOnDisk;
return DokanResult.Success;
}
public NtStatus GetFileSecurity(
string fileName,
out FileSystemSecurity? security,
AccessControlSections sections,
IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
var node = _storage.GetNode(path);
if (node == null)
{
security = null;
return DokanResult.FileNotFound;
}
security = node.Security;
return DokanResult.Success;
}
public NtStatus SetFileSecurity(
string fileName,
FileSystemSecurity security,
AccessControlSections sections,
IDokanFileInfo info)
{
var path = FloppyStorage.NormalizePath(fileName);
var node = _storage.GetNode(path);
if (node == null)
{
return DokanResult.FileNotFound;
}
node.Security = security;
return DokanResult.Success;
}
public NtStatus Mounted(string mountPoint, IDokanFileInfo info)
{
Console.WriteLine($"Drive mounted at {mountPoint}");
return DokanResult.Success;
}
public NtStatus Unmounted(IDokanFileInfo info)
{
Console.WriteLine("Drive unmounted");
return DokanResult.Success;
}
public NtStatus FindStreams(
string fileName,
out IList<FileInformation> streams,
IDokanFileInfo info)
{
streams = [];
return DokanResult.NotImplemented;
}
}

View File

@@ -1,213 +0,0 @@
using System.Collections.Concurrent;
namespace DevDrive;
/// <summary>
/// Manages the in-memory file system storage for the virtual drive.
/// </summary>
public class FloppyStorage
{
public const long DefaultCapacity = 1_474_560;
public const int SectorSize = 512;
public const int AllocationUnitSize = 512;
private readonly ConcurrentDictionary<string, FloppyFileNode> _files = new(StringComparer.OrdinalIgnoreCase);
private readonly object _lock = new();
public long Capacity { get; }
public long FreeSpace { get; }
public FloppyStorage() : this(DefaultCapacity, DefaultCapacity)
{
}
public FloppyStorage(long totalBytes, long freeBytes)
{
Capacity = totalBytes;
FreeSpace = freeBytes;
var root = new FloppyFileNode("\\", true);
_files["\\"] = root;
}
public FloppyFileNode? CreateSymlink(string path, string targetPath, bool isDirectory)
{
path = NormalizePath(path);
if (_files.ContainsKey(path))
{
return null;
}
var parentPath = GetParentPath(path);
if (!_files.ContainsKey(parentPath))
{
return null;
}
var node = new FloppyFileNode(path, isDirectory)
{
SymlinkTarget = targetPath,
Attributes = (isDirectory ? FileAttributes.Directory : FileAttributes.Normal) | FileAttributes.ReparsePoint
};
_files[path] = node;
return node;
}
public static string NormalizePath(string path)
{
if (string.IsNullOrEmpty(path))
{
return "\\";
}
path = path.Replace('/', '\\');
if (!path.StartsWith('\\'))
{
path = "\\" + path;
}
while (path.Contains("\\\\"))
{
path = path.Replace("\\\\", "\\");
}
if (path.Length > 1 && path.EndsWith('\\'))
{
path = path.TrimEnd('\\');
}
return path;
}
public static string GetParentPath(string path)
{
path = NormalizePath(path);
if (path == "\\")
{
return "\\";
}
var lastSlash = path.LastIndexOf('\\');
return lastSlash <= 0 ? "\\" : path[..lastSlash];
}
public bool Exists(string path)
{
path = NormalizePath(path);
return _files.ContainsKey(path);
}
public FloppyFileNode? GetNode(string path)
{
path = NormalizePath(path);
_files.TryGetValue(path, out var node);
return node;
}
public FloppyFileNode? CreateNode(string path, bool isDirectory)
{
path = NormalizePath(path);
if (_files.ContainsKey(path))
{
return _files[path];
}
var parentPath = GetParentPath(path);
if (!_files.ContainsKey(parentPath))
{
return null;
}
var node = new FloppyFileNode(path, isDirectory);
_files[path] = node;
return node;
}
public bool DeleteNode(string path)
{
path = NormalizePath(path);
if (path == "\\")
{
return false;
}
var node = GetNode(path);
if (node == null)
{
return false;
}
if (node.IsDirectory)
{
var children = GetChildren(path);
if (children.Any())
{
return false;
}
}
return _files.TryRemove(path, out _);
}
public IEnumerable<FloppyFileNode> GetChildren(string path)
{
path = NormalizePath(path);
foreach (var kvp in _files)
{
var filePath = kvp.Key;
if (filePath == path)
{
continue;
}
var parentPath = GetParentPath(filePath);
if (string.Equals(parentPath, path, StringComparison.OrdinalIgnoreCase))
{
yield return kvp.Value;
}
}
}
public bool MoveNode(string oldPath, string newPath, bool replace)
{
oldPath = NormalizePath(oldPath);
newPath = NormalizePath(newPath);
if (!_files.TryGetValue(oldPath, out var node))
{
return false;
}
if (_files.ContainsKey(newPath))
{
if (!replace)
{
return false;
}
_files.TryRemove(newPath, out _);
}
var newParentPath = GetParentPath(newPath);
if (!_files.ContainsKey(newParentPath))
{
return false;
}
_files.TryRemove(oldPath, out _);
node.FullPath = newPath;
node.Name = Path.GetFileName(newPath);
node.LastWriteTime = DateTime.Now;
_files[newPath] = node;
return true;
}
public bool HasEnoughSpace(long additionalBytes)
{
return FreeSpace >= additionalBytes;
}
}

471
MirrorFileSystem.cs Normal file
View File

@@ -0,0 +1,471 @@
using System.Security.AccessControl;
using DokanNet;
using FileAccess = DokanNet.FileAccess;
namespace DevDrive;
/// <summary>
/// A Dokan filesystem that mirrors a real folder but reports custom capacity.
/// </summary>
public class MirrorFileSystem : IDokanOperations
{
private const string FileSystemName = "NTFS";
private const uint VolumeSerial = 0x19950101;
private readonly string _rootPath;
private string _volumeName;
private long _totalBytes;
private long _freeBytes;
/// <summary>
/// Creates a new mirror filesystem.
/// </summary>
/// <param name="rootPath">The real folder to mirror.</param>
/// <param name="volumeName">The volume name to display.</param>
/// <param name="totalBytes">Fake total capacity in bytes.</param>
/// <param name="freeBytes">Fake free space in bytes.</param>
public MirrorFileSystem(string rootPath, string volumeName, long totalBytes, long freeBytes)
{
_rootPath = rootPath;
_volumeName = volumeName;
_totalBytes = totalBytes;
_freeBytes = freeBytes;
}
/// <summary>
/// Updates the volume metadata (name and capacity) without remounting.
/// </summary>
/// <param name="volumeName">New volume name.</param>
/// <param name="totalBytes">New total capacity in bytes.</param>
/// <param name="freeBytes">New free space in bytes.</param>
public void UpdateMetadata(string volumeName, long totalBytes, long freeBytes)
{
_volumeName = volumeName;
_totalBytes = totalBytes;
_freeBytes = freeBytes;
}
private string GetRealPath(string fileName)
{
var path = fileName.TrimStart('\\');
return string.IsNullOrEmpty(path) ? _rootPath : Path.Combine(_rootPath, path);
}
public NtStatus CreateFile(string fileName, FileAccess access, FileShare share, FileMode mode,
FileOptions options, FileAttributes attributes, IDokanFileInfo info)
{
var realPath = GetRealPath(fileName);
if (info.IsDirectory)
{
try
{
switch (mode)
{
case FileMode.CreateNew:
if (Directory.Exists(realPath))
return DokanResult.FileExists;
Directory.CreateDirectory(realPath);
return DokanResult.Success;
case FileMode.Open:
if (!Directory.Exists(realPath))
return DokanResult.PathNotFound;
return DokanResult.Success;
default:
if (!Directory.Exists(realPath))
return DokanResult.PathNotFound;
return DokanResult.Success;
}
}
catch (UnauthorizedAccessException)
{
return DokanResult.AccessDenied;
}
}
var pathExists = File.Exists(realPath);
var dirExists = Directory.Exists(realPath);
if (dirExists)
{
info.IsDirectory = true;
return DokanResult.Success;
}
try
{
switch (mode)
{
case FileMode.Open:
if (!pathExists)
return DokanResult.FileNotFound;
break;
case FileMode.CreateNew:
if (pathExists)
return DokanResult.FileExists;
break;
case FileMode.Truncate:
if (!pathExists)
return DokanResult.FileNotFound;
break;
}
var fileAccess = System.IO.FileAccess.Read;
if (access.HasFlag(FileAccess.WriteData) || access.HasFlag(FileAccess.AppendData) ||
access.HasFlag(FileAccess.Delete) || access.HasFlag(FileAccess.GenericWrite))
{
fileAccess = System.IO.FileAccess.ReadWrite;
}
info.Context = new FileStream(realPath, mode, fileAccess, share, 4096, options);
}
catch (UnauthorizedAccessException)
{
return DokanResult.AccessDenied;
}
catch (DirectoryNotFoundException)
{
return DokanResult.PathNotFound;
}
catch (FileNotFoundException)
{
return DokanResult.FileNotFound;
}
catch (IOException)
{
return DokanResult.SharingViolation;
}
return DokanResult.Success;
}
public void Cleanup(string fileName, IDokanFileInfo info)
{
(info.Context as FileStream)?.Dispose();
info.Context = null;
}
public void CloseFile(string fileName, IDokanFileInfo info)
{
(info.Context as FileStream)?.Dispose();
info.Context = null;
}
public NtStatus ReadFile(string fileName, byte[] buffer, out int bytesRead, long offset, IDokanFileInfo info)
{
bytesRead = 0;
if (info.Context is FileStream stream)
{
lock (stream)
{
stream.Position = offset;
bytesRead = stream.Read(buffer, 0, buffer.Length);
}
return DokanResult.Success;
}
var realPath = GetRealPath(fileName);
try
{
using var fs = new FileStream(realPath, FileMode.Open, System.IO.FileAccess.Read, FileShare.ReadWrite);
fs.Position = offset;
bytesRead = fs.Read(buffer, 0, buffer.Length);
return DokanResult.Success;
}
catch
{
return DokanResult.Error;
}
}
public NtStatus WriteFile(string fileName, byte[] buffer, out int bytesWritten, long offset, IDokanFileInfo info)
{
bytesWritten = 0;
if (info.Context is FileStream stream)
{
lock (stream)
{
stream.Position = offset;
stream.Write(buffer, 0, buffer.Length);
}
bytesWritten = buffer.Length;
return DokanResult.Success;
}
var realPath = GetRealPath(fileName);
try
{
using var fs = new FileStream(realPath, FileMode.Open, System.IO.FileAccess.Write, FileShare.ReadWrite);
fs.Position = offset;
fs.Write(buffer, 0, buffer.Length);
bytesWritten = buffer.Length;
return DokanResult.Success;
}
catch
{
return DokanResult.Error;
}
}
public NtStatus FlushFileBuffers(string fileName, IDokanFileInfo info)
{
try
{
(info.Context as FileStream)?.Flush();
return DokanResult.Success;
}
catch
{
return DokanResult.Error;
}
}
public NtStatus GetFileInformation(string fileName, out FileInformation fileInfo, IDokanFileInfo info)
{
var realPath = GetRealPath(fileName);
fileInfo = new FileInformation();
if (File.Exists(realPath))
{
var fi = new FileInfo(realPath);
fileInfo.FileName = fi.Name;
fileInfo.Attributes = fi.Attributes;
fileInfo.CreationTime = fi.CreationTime;
fileInfo.LastAccessTime = fi.LastAccessTime;
fileInfo.LastWriteTime = fi.LastWriteTime;
fileInfo.Length = fi.Length;
return DokanResult.Success;
}
if (Directory.Exists(realPath))
{
var di = new DirectoryInfo(realPath);
fileInfo.FileName = di.Name;
fileInfo.Attributes = di.Attributes;
fileInfo.CreationTime = di.CreationTime;
fileInfo.LastAccessTime = di.LastAccessTime;
fileInfo.LastWriteTime = di.LastWriteTime;
fileInfo.Length = 0;
return DokanResult.Success;
}
return DokanResult.FileNotFound;
}
public NtStatus FindFiles(string fileName, out IList<FileInformation> files, IDokanFileInfo info)
{
return FindFilesWithPattern(fileName, "*", out files, info);
}
public NtStatus FindFilesWithPattern(string fileName, string searchPattern, out IList<FileInformation> files, IDokanFileInfo info)
{
files = new List<FileInformation>();
var realPath = GetRealPath(fileName);
if (!Directory.Exists(realPath))
return DokanResult.PathNotFound;
try
{
var dirInfo = new DirectoryInfo(realPath);
foreach (var fsi in dirInfo.EnumerateFileSystemInfos(searchPattern))
{
files.Add(new FileInformation
{
FileName = fsi.Name,
Attributes = fsi.Attributes,
CreationTime = fsi.CreationTime,
LastAccessTime = fsi.LastAccessTime,
LastWriteTime = fsi.LastWriteTime,
Length = fsi is FileInfo fi ? fi.Length : 0
});
}
}
catch
{
return DokanResult.Error;
}
return DokanResult.Success;
}
public NtStatus SetFileAttributes(string fileName, FileAttributes attributes, IDokanFileInfo info)
{
try
{
File.SetAttributes(GetRealPath(fileName), attributes);
return DokanResult.Success;
}
catch
{
return DokanResult.Error;
}
}
public NtStatus SetFileTime(string fileName, DateTime? creationTime, DateTime? lastAccessTime,
DateTime? lastWriteTime, IDokanFileInfo info)
{
var realPath = GetRealPath(fileName);
try
{
if (creationTime.HasValue)
File.SetCreationTime(realPath, creationTime.Value);
if (lastAccessTime.HasValue)
File.SetLastAccessTime(realPath, lastAccessTime.Value);
if (lastWriteTime.HasValue)
File.SetLastWriteTime(realPath, lastWriteTime.Value);
return DokanResult.Success;
}
catch
{
return DokanResult.Error;
}
}
public NtStatus DeleteFile(string fileName, IDokanFileInfo info)
{
var realPath = GetRealPath(fileName);
if (!File.Exists(realPath))
return DokanResult.FileNotFound;
return DokanResult.Success;
}
public NtStatus DeleteDirectory(string fileName, IDokanFileInfo info)
{
var realPath = GetRealPath(fileName);
if (!Directory.Exists(realPath))
return DokanResult.PathNotFound;
if (Directory.EnumerateFileSystemEntries(realPath).Any())
return DokanResult.DirectoryNotEmpty;
return DokanResult.Success;
}
public NtStatus MoveFile(string oldName, string newName, bool replace, IDokanFileInfo info)
{
var oldPath = GetRealPath(oldName);
var newPath = GetRealPath(newName);
(info.Context as FileStream)?.Dispose();
info.Context = null;
try
{
if (info.IsDirectory)
{
if (Directory.Exists(newPath))
{
if (!replace)
return DokanResult.FileExists;
Directory.Delete(newPath, true);
}
Directory.Move(oldPath, newPath);
}
else
{
if (File.Exists(newPath))
{
if (!replace)
return DokanResult.FileExists;
File.Delete(newPath);
}
File.Move(oldPath, newPath);
}
return DokanResult.Success;
}
catch
{
return DokanResult.Error;
}
}
public NtStatus SetEndOfFile(string fileName, long length, IDokanFileInfo info)
{
try
{
if (info.Context is FileStream stream)
{
stream.SetLength(length);
}
else
{
using var fs = new FileStream(GetRealPath(fileName), FileMode.Open, System.IO.FileAccess.Write);
fs.SetLength(length);
}
return DokanResult.Success;
}
catch
{
return DokanResult.Error;
}
}
public NtStatus SetAllocationSize(string fileName, long length, IDokanFileInfo info)
{
return SetEndOfFile(fileName, length, info);
}
public NtStatus LockFile(string fileName, long offset, long length, IDokanFileInfo info)
{
return DokanResult.Success;
}
public NtStatus UnlockFile(string fileName, long offset, long length, IDokanFileInfo info)
{
return DokanResult.Success;
}
public NtStatus GetDiskFreeSpace(out long freeBytesAvailable, out long totalNumberOfBytes,
out long totalNumberOfFreeBytes, IDokanFileInfo info)
{
freeBytesAvailable = _freeBytes;
totalNumberOfBytes = _totalBytes;
totalNumberOfFreeBytes = _freeBytes;
return DokanResult.Success;
}
public NtStatus GetVolumeInformation(out string volumeLabel, out FileSystemFeatures features,
out string fileSystemName, out uint maximumComponentLength, IDokanFileInfo info)
{
volumeLabel = _volumeName;
features = FileSystemFeatures.CasePreservedNames | FileSystemFeatures.CaseSensitiveSearch |
FileSystemFeatures.SupportsRemoteStorage | FileSystemFeatures.UnicodeOnDisk;
fileSystemName = FileSystemName;
maximumComponentLength = 256;
return DokanResult.Success;
}
public NtStatus GetFileSecurity(string fileName, out FileSystemSecurity? security,
AccessControlSections sections, IDokanFileInfo info)
{
security = null;
return DokanResult.NotImplemented;
}
public NtStatus SetFileSecurity(string fileName, FileSystemSecurity security,
AccessControlSections sections, IDokanFileInfo info)
{
return DokanResult.NotImplemented;
}
public NtStatus Mounted(string mountPoint, IDokanFileInfo info)
{
return DokanResult.Success;
}
public NtStatus Unmounted(IDokanFileInfo info)
{
return DokanResult.Success;
}
public NtStatus FindStreams(string fileName, out IList<FileInformation> streams, IDokanFileInfo info)
{
streams = Array.Empty<FileInformation>();
return DokanResult.NotImplemented;
}
}

View File

@@ -7,53 +7,66 @@ using DokanNet.Logging;
namespace DevDrive; namespace DevDrive;
/// <summary> /// <summary>
/// Entry point for the virtual drive application. /// Entry point for the DevDrive application.
/// Uses Dokan to mount a virtual drive with fake capacity and real mklink symlinks.
/// </summary> /// </summary>
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
public static class Program public static class Program
{ {
private const string DefaultMountPoint = "A:\\"; private const string DefaultDriveLetter = "A:";
private const string DefaultConfigDrive = "C:\\PhysicalDrive\\"; private const string DefaultConfigPath = "C:\\PhysicalDrive\\";
private const string ConfigFile = "index.json"; private const string ConfigFile = "index.json";
private const string LogPrefix = "[DevDrive]"; private const string LogPrefix = "[DevDrive]";
private const string DevDriveFolderName = "DevDriveRoot";
private static readonly ManualResetEvent ShutdownEvent = new(false);
private static string _driveLetter = DefaultDriveLetter;
private static string _mountPoint = "A:\\";
private static string _configPath = DefaultConfigPath;
private static string _configFilePath = "";
private static string _devDrivePath = "";
private static string _devConfigPath = "";
private static DevDriveConfig? _lastConfig;
private static readonly object SyncLock = new();
private static FileSystemWatcher? _devConfigWatcher;
private static Dokan? _dokan; private static Dokan? _dokan;
private static DokanInstance? _dokanInstance; private static DokanInstance? _dokanInstance;
private static readonly ManualResetEvent ShutdownEvent = new(false); private static MirrorFileSystem? _mirrorFs;
private static bool _isMounted; private static bool _isMounted;
private static string _configDrive = DefaultConfigDrive; private static bool _isUpdatingDevConfig;
private static string _configFilePath = "";
private static string _mountPoint = DefaultMountPoint;
private static FloppyConfig? _lastConfig;
private static readonly object MountLock = new();
private static bool _isConfigDriveLetter; private static bool _isConfigDriveLetter;
/// <summary> /// <summary>
/// Main entry point. /// Main entry point.
/// </summary> /// </summary>
/// <param name="args">Command line arguments. [0] = mount point, [1] = config drive.</param> /// <param name="args">Command line arguments. [0] = drive letter (e.g. "A:"), [1] = config path.</param>
public static void Main(string[] args) public static void Main(string[] args)
{ {
_mountPoint = args.Length > 0 ? args[0] : DefaultMountPoint; _driveLetter = args.Length > 0 ? args[0].TrimEnd('\\') : DefaultDriveLetter;
_configDrive = args.Length > 1 ? args[1] : DefaultConfigDrive; _configPath = args.Length > 1 ? args[1] : DefaultConfigPath;
if (!_mountPoint.EndsWith("\\")) if (!_driveLetter.EndsWith(":"))
{ {
_mountPoint += "\\"; _driveLetter += ":";
} }
if (!_configDrive.EndsWith("\\")) _mountPoint = _driveLetter + "\\";
if (!_configPath.EndsWith("\\"))
{ {
_configDrive += "\\"; _configPath += "\\";
} }
_configFilePath = Path.Combine(_configDrive, ConfigFile); var exeDir = AppContext.BaseDirectory;
_isConfigDriveLetter = _configDrive.Length <= 3 && _configDrive[1] == ':'; _devDrivePath = Path.Combine(exeDir, DevDriveFolderName);
_devConfigPath = Path.Combine(_devDrivePath, "devDriveConfig.json");
_configFilePath = Path.Combine(_configPath, ConfigFile);
_isConfigDriveLetter = _configPath.Length <= 3 && _configPath[1] == ':';
Console.WriteLine($"{LogPrefix} DevDrive"); Console.WriteLine($"{LogPrefix} DevDrive (Dokan + mklink mode)");
Console.WriteLine($"{LogPrefix} =================="); Console.WriteLine($"{LogPrefix} ==============================");
Console.WriteLine($"{LogPrefix} Watching for config drive: {_configDrive}"); Console.WriteLine($"{LogPrefix} Drive letter: {_driveLetter}");
Console.WriteLine($"{LogPrefix} Config file: {_configFilePath}"); Console.WriteLine($"{LogPrefix} Config file: {_configFilePath}");
Console.WriteLine($"{LogPrefix} Mount point: {_mountPoint}"); Console.WriteLine($"{LogPrefix} Root folder: {_devDrivePath}");
Console.WriteLine(); Console.WriteLine();
Console.WriteLine($"{LogPrefix} Press Ctrl+C to exit."); Console.WriteLine($"{LogPrefix} Press Ctrl+C to exit.");
Console.WriteLine(); Console.WriteLine();
@@ -68,6 +81,8 @@ public static class Program
try try
{ {
_dokan = new Dokan(new ConsoleLogger(LogPrefix)); _dokan = new Dokan(new ConsoleLogger(LogPrefix));
EnsureRootFolder();
SetupDevConfigWatcher();
if (_isConfigDriveLetter) if (_isConfigDriveLetter)
{ {
@@ -75,7 +90,7 @@ public static class Program
} }
else else
{ {
StartFileSystemWatcher(); StartVolumeWatchers();
} }
CheckAndMountIfReady(); CheckAndMountIfReady();
@@ -89,17 +104,54 @@ public static class Program
} }
finally finally
{ {
UnmountFloppy(); CleanupSymlinks();
UnmountDrive();
_devConfigWatcher?.Dispose();
_dokan?.Dispose(); _dokan?.Dispose();
} }
} }
private static void StartFileSystemWatcher() private static void EnsureRootFolder()
{ {
StartVolumeWatchers(); if (!Directory.Exists(_devDrivePath))
{
Directory.CreateDirectory(_devDrivePath);
Console.WriteLine($"{LogPrefix} Created root folder: {_devDrivePath}");
}
}
Console.WriteLine($"{LogPrefix} Volume watchers started for {_configDrive}"); private static bool IsConfigAccessible()
Console.WriteLine($"{LogPrefix} Watching for: {ConfigFile}"); {
try
{
return Directory.Exists(_configPath) && File.Exists(_configFilePath);
}
catch
{
return false;
}
}
private static void StartWmiWatchers()
{
var driveLetter = _configPath.TrimEnd('\\');
var insertQuery = new WqlEventQuery(
"SELECT * FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_LogicalDisk'"
);
var insertWatcher = new ManagementEventWatcher(insertQuery);
insertWatcher.EventArrived += OnDriveInserted;
insertWatcher.Start();
var removeQuery = new WqlEventQuery(
"SELECT * FROM __InstanceDeletionEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_LogicalDisk'"
);
var removeWatcher = new ManagementEventWatcher(removeQuery);
removeWatcher.EventArrived += OnDriveRemoved;
removeWatcher.Start();
Console.WriteLine($"{LogPrefix} WMI watchers started (event-based, no polling)");
Console.WriteLine($"{LogPrefix} Watching for drive: {driveLetter}");
} }
private static void StartVolumeWatchers() private static void StartVolumeWatchers()
@@ -125,17 +177,19 @@ public static class Program
removeWatcher.EventArrived += (s, e) => removeWatcher.EventArrived += (s, e) =>
{ {
Console.WriteLine($"{LogPrefix} Volume removed"); Console.WriteLine($"{LogPrefix} Volume removed");
lock (MountLock) lock (SyncLock)
{ {
if (_isMounted && !IsConfigAccessible()) if (_isMounted && !IsConfigAccessible())
{ {
Console.WriteLine($"{LogPrefix} Config drive no longer accessible, unmounting..."); Console.WriteLine($"{LogPrefix} Config drive no longer accessible, unmounting...");
UnmountFloppy(); UnmountDrive();
_lastConfig = null; _lastConfig = null;
} }
} }
}; };
removeWatcher.Start(); removeWatcher.Start();
Console.WriteLine($"{LogPrefix} Volume watchers started for {_configPath}");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -143,46 +197,6 @@ public static class Program
} }
} }
private static bool IsConfigAccessible()
{
try
{
if (!Directory.Exists(_configDrive))
{
return false;
}
var _ = Directory.GetFiles(_configDrive);
return File.Exists(_configFilePath);
}
catch
{
return false;
}
}
private static void StartWmiWatchers()
{
var driveLetter = _configDrive.TrimEnd('\\');
var insertQuery = new WqlEventQuery(
"SELECT * FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_LogicalDisk'"
);
var insertWatcher = new ManagementEventWatcher(insertQuery);
insertWatcher.EventArrived += OnDriveInserted;
insertWatcher.Start();
var removeQuery = new WqlEventQuery(
"SELECT * FROM __InstanceDeletionEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_LogicalDisk'"
);
var removeWatcher = new ManagementEventWatcher(removeQuery);
removeWatcher.EventArrived += OnDriveRemoved;
removeWatcher.Start();
Console.WriteLine($"{LogPrefix} WMI watchers started (event-based, no polling)");
Console.WriteLine($"{LogPrefix} Watching for drive: {driveLetter}");
}
private static void OnDriveInserted(object sender, EventArrivedEventArgs e) private static void OnDriveInserted(object sender, EventArrivedEventArgs e)
{ {
try try
@@ -197,7 +211,7 @@ public static class Program
var insertedDrive = deviceId + "\\"; var insertedDrive = deviceId + "\\";
if (string.Equals(insertedDrive, _configDrive, StringComparison.OrdinalIgnoreCase)) if (string.Equals(insertedDrive, _configPath, StringComparison.OrdinalIgnoreCase))
{ {
Console.WriteLine(); Console.WriteLine();
Console.WriteLine($"{LogPrefix} Drive {deviceId} inserted!"); Console.WriteLine($"{LogPrefix} Drive {deviceId} inserted!");
@@ -226,17 +240,17 @@ public static class Program
var removedDrive = deviceId + "\\"; var removedDrive = deviceId + "\\";
if (string.Equals(removedDrive, _configDrive, StringComparison.OrdinalIgnoreCase)) if (string.Equals(removedDrive, _configPath, StringComparison.OrdinalIgnoreCase))
{ {
Console.WriteLine(); Console.WriteLine();
Console.WriteLine($"{LogPrefix} Drive {deviceId} removed!"); Console.WriteLine($"{LogPrefix} Drive {deviceId} removed!");
lock (MountLock) lock (SyncLock)
{ {
if (_isMounted) if (_isMounted)
{ {
Console.WriteLine($"{LogPrefix} Config drive disconnected, unmounting..."); Console.WriteLine($"{LogPrefix} Config drive disconnected, unmounting...");
UnmountFloppy(); UnmountDrive();
_lastConfig = null; _lastConfig = null;
} }
} }
@@ -250,14 +264,14 @@ public static class Program
private static void CheckAndMountIfReady() private static void CheckAndMountIfReady()
{ {
lock (MountLock) lock (SyncLock)
{ {
var driveExists = Directory.Exists(_configDrive); var driveExists = Directory.Exists(_configPath);
var configExists = driveExists && File.Exists(_configFilePath); var configExists = driveExists && File.Exists(_configFilePath);
if (!driveExists) if (!driveExists)
{ {
Console.WriteLine($"{LogPrefix} Waiting for {_configDrive} to be connected..."); Console.WriteLine($"{LogPrefix} Waiting for {_configPath} to be connected...");
return; return;
} }
@@ -275,111 +289,52 @@ public static class Program
if (_isMounted) if (_isMounted)
{ {
UnmountFloppy(); UnmountDrive();
_lastConfig = null; _lastConfig = null;
} }
return; return;
} }
var configChanged = !ConfigEquals(_lastConfig, config); if (!_isMounted)
if (!_isMounted || configChanged)
{ {
if (_isMounted && configChanged) EnsureDevConfigFromConfig(config);
{ MountDrive();
Console.WriteLine($"{LogPrefix} Config changed, remounting..."); SyncSymlinksFromConfig(config);
UnmountFloppy();
}
MountFloppy(config);
_lastConfig = config; _lastConfig = config;
} }
} }
} }
private static FloppyConfig? ReadConfig(string configFilePath) private static void MountDrive()
{ {
try if (_dokan == null || _isMounted)
{
var json = File.ReadAllText(configFilePath);
return JsonSerializer.Deserialize<FloppyConfig>(json);
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error reading config: {ex.Message}");
return null;
}
}
private static bool ConfigEquals(FloppyConfig? a, FloppyConfig? b)
{
if (a == null || b == null)
{
return a == b;
}
if (a.Paths.Count != b.Paths.Count)
{
return false;
}
for (var i = 0; i < a.Paths.Count; i++)
{
if (a.Paths[i].Name != b.Paths[i].Name || a.Paths[i].Path != b.Paths[i].Path)
{
return false;
}
}
return true;
}
private static void MountFloppy(FloppyConfig config)
{
if (_dokan == null)
{ {
return; return;
} }
var config = ReadConfig(_configFilePath);
var volumeName = config?.DriveName ?? "Dev Drive";
var totalBytes = (long)((config?.DriveTotalMB ?? 1.44) * 1024 * 1024);
var freeBytes = (long)((config?.DriveFreeMB ?? 0.72) * 1024 * 1024);
try try
{ {
var symlinks = new Dictionary<string, string>(); _mirrorFs = new MirrorFileSystem(_devDrivePath, volumeName, totalBytes, freeBytes);
Console.WriteLine();
Console.WriteLine($"{LogPrefix} Creating symlinks:");
foreach (var entry in config.Paths)
{
var symlinkPath = "\\" + entry.Name;
symlinks[symlinkPath] = entry.Path;
Console.WriteLine($"{LogPrefix} {entry.Name} -> {entry.Path}");
}
Console.WriteLine();
var totalBytes = (long)(config.DriveTotalMB * 1024 * 1024);
var freeBytes = (long)(config.DriveFreeMB * 1024 * 1024);
Console.WriteLine($"{LogPrefix} Drive size: {config.DriveTotalMB} MB total, {config.DriveFreeMB} MB free");
var floppyFs = new FloppyFileSystem(symlinks, config.DriveName, totalBytes, freeBytes);
var dokanBuilder = new DokanInstanceBuilder(_dokan) var dokanBuilder = new DokanInstanceBuilder(_dokan)
.ConfigureOptions(options => .ConfigureOptions(options =>
{ {
options.Options = DokanOptions.MountManager | DokanOptions.CurrentSession; options.Options = DokanOptions.FixedDrive | DokanOptions.CurrentSession;
options.MountPoint = _mountPoint; options.MountPoint = _mountPoint;
options.SingleThread = false; options.SingleThread = false;
options.AllocationUnitSize = FloppyStorage.AllocationUnitSize;
options.SectorSize = FloppyStorage.SectorSize;
}); });
_dokanInstance = dokanBuilder.Build(floppyFs); _dokanInstance = dokanBuilder.Build(_mirrorFs);
_isMounted = true; _isMounted = true;
Console.WriteLine($"{LogPrefix} Mounted at {_mountPoint}"); Console.WriteLine($"{LogPrefix} Mounted {_driveLetter} (Dokan)");
Console.WriteLine($"{LogPrefix} Volume: {volumeName}");
var iconPath = string.IsNullOrWhiteSpace(config.DriveIcon) Console.WriteLine($"{LogPrefix} Capacity: {config?.DriveTotalMB ?? 1.44} MB total, {config?.DriveFreeMB ?? 0.72} MB free");
? "%SystemRoot%\\System32\\shell32.dll,6"
: config.DriveIcon;
DriveIconHelper.SetDriveIcon(_mountPoint, iconPath, config.DriveName);
} }
catch (DokanException ex) catch (DokanException ex)
{ {
@@ -388,7 +343,7 @@ public static class Program
} }
} }
private static void UnmountFloppy() private static void UnmountDrive()
{ {
if (!_isMounted) if (!_isMounted)
{ {
@@ -397,16 +352,287 @@ public static class Program
try try
{ {
DriveIconHelper.RemoveDriveIcon();
_dokan?.RemoveMountPoint(_mountPoint); _dokan?.RemoveMountPoint(_mountPoint);
_dokanInstance?.Dispose(); _dokanInstance?.Dispose();
_dokanInstance = null; _dokanInstance = null;
_mirrorFs = null;
_isMounted = false; _isMounted = false;
Console.WriteLine($"{LogPrefix} Unmounted"); Console.WriteLine($"{LogPrefix} Unmounted {_driveLetter}");
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"{LogPrefix} Unmount error: {ex.Message}"); Console.WriteLine($"{LogPrefix} Unmount error: {ex.Message}");
} }
} }
private static void SetupDevConfigWatcher()
{
if (_devConfigWatcher != null)
{
return;
}
if (!Directory.Exists(_devDrivePath))
{
return;
}
_devConfigWatcher = new FileSystemWatcher(_devDrivePath)
{
Filter = "devDriveConfig.json",
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime | NotifyFilters.FileName,
EnableRaisingEvents = true
};
_devConfigWatcher.Changed += OnDevConfigChanged;
_devConfigWatcher.Created += OnDevConfigChanged;
Console.WriteLine($"{LogPrefix} Watching devDriveConfig.json for changes...");
}
private static void OnDevConfigChanged(object sender, FileSystemEventArgs e)
{
lock (SyncLock)
{
if (_isUpdatingDevConfig)
{
// Change originated from real config sync, ignore.
_isUpdatingDevConfig = false;
return;
}
}
Thread.Sleep(100);
try
{
if (!File.Exists(_devConfigPath))
{
return;
}
var json = File.ReadAllText(_devConfigPath);
var config = JsonSerializer.Deserialize<DevDriveConfig>(json);
if (config == null)
{
Console.WriteLine($"{LogPrefix} devDriveConfig.json change could not be deserialized.");
return;
}
lock (SyncLock)
{
File.WriteAllText(_configFilePath, json);
// Check if drive metadata changed (name, capacity)
var metadataChanged = _lastConfig == null ||
_lastConfig.DriveName != config.DriveName ||
_lastConfig.DriveTotalMB != config.DriveTotalMB ||
_lastConfig.DriveFreeMB != config.DriveFreeMB;
if (metadataChanged && _mirrorFs != null)
{
var volumeName = config.DriveName ?? "Dev Drive";
var totalBytes = (long)(config.DriveTotalMB * 1024 * 1024);
var freeBytes = (long)(config.DriveFreeMB * 1024 * 1024);
_mirrorFs.UpdateMetadata(volumeName, totalBytes, freeBytes);
Console.WriteLine($"{LogPrefix} Updated drive metadata: {volumeName}, {config.DriveTotalMB} MB total, {config.DriveFreeMB} MB free");
}
SyncSymlinksFromConfig(config);
_lastConfig = config;
}
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error processing devDriveConfig.json change: {ex.Message}");
}
}
private static DevDriveConfig? ReadConfig(string path)
{
try
{
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<DevDriveConfig>(json);
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error reading config: {ex.Message}");
return null;
}
}
private static void SyncSymlinksFromConfig(DevDriveConfig config)
{
Console.WriteLine();
Console.WriteLine($"{LogPrefix} Syncing symlinks (mklink /d):");
var existingEntries = Directory.GetFileSystemEntries(_devDrivePath)
.Where(p => !Path.GetFileName(p).Equals("desktop.ini", StringComparison.OrdinalIgnoreCase))
.Where(p => !Path.GetFileName(p).Equals("devDriveConfig.json", StringComparison.OrdinalIgnoreCase))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var configPaths = config.Paths
.Select(p => Path.Combine(_devDrivePath, p.Name))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
foreach (var entry in existingEntries)
{
if (!configPaths.Contains(entry))
{
RemoveSymlink(entry);
}
}
foreach (var pathEntry in config.Paths)
{
var symlinkPath = Path.Combine(_devDrivePath, pathEntry.Name);
CreateSymlink(symlinkPath, pathEntry.Path);
}
Console.WriteLine();
}
private static void EnsureDevConfigFromConfig(DevDriveConfig config)
{
try
{
_isUpdatingDevConfig = true;
var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(_devConfigPath, json);
Console.WriteLine($"{LogPrefix} Created devDriveConfig.json from index.json");
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error creating devDriveConfig.json: {ex.Message}");
}
}
private static void CreateSymlink(string symlinkPath, string targetPath)
{
try
{
if (Directory.Exists(symlinkPath) || File.Exists(symlinkPath))
{
var attr = File.GetAttributes(symlinkPath);
if (attr.HasFlag(FileAttributes.ReparsePoint))
{
var existingTarget = new FileInfo(symlinkPath).LinkTarget;
if (existingTarget?.Equals(targetPath, StringComparison.OrdinalIgnoreCase) == true)
{
Console.WriteLine($"{LogPrefix} {Path.GetFileName(symlinkPath)} -> {targetPath} (unchanged)");
return;
}
RemoveSymlink(symlinkPath);
}
else
{
Console.WriteLine($"{LogPrefix} Skipping {Path.GetFileName(symlinkPath)} - not a symlink");
return;
}
}
var isDirectory = Directory.Exists(targetPath);
var args = isDirectory
? $"/c mklink /d \"{symlinkPath}\" \"{targetPath}\""
: $"/c mklink \"{symlinkPath}\" \"{targetPath}\"";
var result = RunCommand("cmd.exe", args);
if (result.ExitCode == 0)
{
Console.WriteLine($"{LogPrefix} {Path.GetFileName(symlinkPath)} -> {targetPath}");
}
else
{
Console.WriteLine($"{LogPrefix} Failed: {Path.GetFileName(symlinkPath)} - {result.Error}");
}
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error: {Path.GetFileName(symlinkPath)} - {ex.Message}");
}
}
private static void RemoveSymlink(string path)
{
try
{
var attr = File.GetAttributes(path);
if (attr.HasFlag(FileAttributes.Directory))
{
Directory.Delete(path, false);
}
else
{
File.Delete(path);
}
Console.WriteLine($"{LogPrefix} Removed: {Path.GetFileName(path)}");
}
catch (Exception ex)
{
Console.WriteLine($"{LogPrefix} Error removing {Path.GetFileName(path)}: {ex.Message}");
}
}
private static void CleanupSymlinks()
{
if (!Directory.Exists(_devDrivePath))
{
return;
}
Console.WriteLine($"{LogPrefix} Cleaning up symlinks...");
var entries = Directory.GetFileSystemEntries(_devDrivePath)
.Where(p => !Path.GetFileName(p).Equals("desktop.ini", StringComparison.OrdinalIgnoreCase))
.Where(p => !Path.GetFileName(p).Equals("devDriveConfig.json", StringComparison.OrdinalIgnoreCase));
foreach (var entry in entries)
{
try
{
var attr = File.GetAttributes(entry);
if (attr.HasFlag(FileAttributes.ReparsePoint))
{
if (attr.HasFlag(FileAttributes.Directory))
{
Directory.Delete(entry, false);
}
else
{
File.Delete(entry);
}
Console.WriteLine($"{LogPrefix} Removed: {Path.GetFileName(entry)}");
}
}
catch
{
}
}
}
private static (int ExitCode, string Output, string Error) RunCommand(string fileName, string arguments)
{
var process = new System.Diagnostics.Process
{
StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
}
};
process.Start();
var output = process.StandardOutput.ReadToEnd();
var error = process.StandardError.ReadToEnd();
process.WaitForExit();
return (process.ExitCode, output.Trim(), error.Trim());
}
} }

View File

@@ -1,201 +0,0 @@
using DokanNet;
namespace DevDrive;
/// <summary>
/// Handles passthrough operations for symlinks to real file system paths.
/// </summary>
public static class SymlinkHandler
{
public static bool TryResolveSymlink(string virtualPath, FloppyStorage storage, out string? realPath)
{
realPath = null;
virtualPath = FloppyStorage.NormalizePath(virtualPath);
var node = storage.GetNode(virtualPath);
if (node?.IsSymlink == true)
{
realPath = node.SymlinkTarget;
return true;
}
var parts = virtualPath.Split('\\', StringSplitOptions.RemoveEmptyEntries);
var currentPath = "\\";
for (var i = 0; i < parts.Length; i++)
{
currentPath = currentPath == "\\" ? "\\" + parts[i] : currentPath + "\\" + parts[i];
var currentNode = storage.GetNode(currentPath);
if (currentNode?.IsSymlink == true)
{
var remainingPath = string.Join("\\", parts.Skip(i + 1));
realPath = string.IsNullOrEmpty(remainingPath)
? currentNode.SymlinkTarget
: Path.Combine(currentNode.SymlinkTarget!, remainingPath);
return true;
}
}
return false;
}
public static IList<FileInformation> ListRealDirectory(string realPath)
{
var files = new List<FileInformation>();
try
{
if (!Directory.Exists(realPath))
{
return files;
}
foreach (var entry in new DirectoryInfo(realPath).EnumerateFileSystemInfos())
{
files.Add(new FileInformation
{
FileName = entry.Name,
Attributes = entry.Attributes,
CreationTime = entry.CreationTime,
LastAccessTime = entry.LastAccessTime,
LastWriteTime = entry.LastWriteTime,
Length = entry is FileInfo fi ? fi.Length : 0
});
}
}
catch
{
}
return files;
}
public static bool GetRealFileInfo(string realPath, out FileInformation fileInfo)
{
fileInfo = default;
try
{
if (File.Exists(realPath))
{
var fi = new FileInfo(realPath);
fileInfo = new FileInformation
{
FileName = fi.Name,
Attributes = fi.Attributes,
CreationTime = fi.CreationTime,
LastAccessTime = fi.LastAccessTime,
LastWriteTime = fi.LastWriteTime,
Length = fi.Length
};
return true;
}
if (Directory.Exists(realPath))
{
var di = new DirectoryInfo(realPath);
fileInfo = new FileInformation
{
FileName = di.Name,
Attributes = di.Attributes,
CreationTime = di.CreationTime,
LastAccessTime = di.LastAccessTime,
LastWriteTime = di.LastWriteTime,
Length = 0
};
return true;
}
}
catch
{
}
return false;
}
public static NtStatus ReadRealFile(string realPath, byte[] buffer, out int bytesRead, long offset)
{
bytesRead = 0;
try
{
using var stream = new FileStream(realPath, System.IO.FileMode.Open, System.IO.FileAccess.Read, FileShare.ReadWrite);
stream.Seek(offset, SeekOrigin.Begin);
bytesRead = stream.Read(buffer, 0, buffer.Length);
return DokanResult.Success;
}
catch (FileNotFoundException)
{
return DokanResult.FileNotFound;
}
catch (UnauthorizedAccessException)
{
return DokanResult.AccessDenied;
}
catch
{
return DokanResult.Error;
}
}
public static NtStatus WriteRealFile(string realPath, byte[] buffer, out int bytesWritten, long offset)
{
bytesWritten = 0;
try
{
using var stream = new FileStream(realPath, System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.Write, FileShare.ReadWrite);
stream.Seek(offset, SeekOrigin.Begin);
stream.Write(buffer, 0, buffer.Length);
bytesWritten = buffer.Length;
return DokanResult.Success;
}
catch (UnauthorizedAccessException)
{
return DokanResult.AccessDenied;
}
catch
{
return DokanResult.Error;
}
}
public static NtStatus CreateRealFileOrDirectory(string realPath, bool isDirectory, FileMode mode)
{
try
{
if (isDirectory)
{
if (mode == FileMode.CreateNew)
{
Directory.CreateDirectory(realPath);
}
return DokanResult.Success;
}
switch (mode)
{
case FileMode.CreateNew:
using (File.Create(realPath)) { }
break;
case FileMode.Create:
case FileMode.OpenOrCreate:
using (new FileStream(realPath, System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.ReadWrite)) { }
break;
default:
break;
}
return DokanResult.Success;
}
catch (UnauthorizedAccessException)
{
return DokanResult.AccessDenied;
}
catch
{
return DokanResult.Error;
}
}
}