diff --git a/DevDriveConfig.cs b/DevDriveConfig.cs
new file mode 100644
index 0000000..f08a759
--- /dev/null
+++ b/DevDriveConfig.cs
@@ -0,0 +1,57 @@
+using System.Text.Json.Serialization;
+
+namespace DevDrive;
+
+///
+/// Represents the index.json configuration file structure.
+///
+public class DevDriveConfig
+{
+ ///
+ /// Display name for the drive.
+ ///
+ [JsonPropertyName("driveName")]
+ public string DriveName { get; set; } = "Dev Drive";
+
+ ///
+ /// Icon path in format "path,index" (e.g. "%SystemRoot%\System32\shell32.dll,8").
+ ///
+ [JsonPropertyName("driveIcon")]
+ public string DriveIcon { get; set; } = "";
+
+ ///
+ /// Total drive capacity in MB (for display purposes).
+ ///
+ [JsonPropertyName("driveTotalMB")]
+ public double DriveTotalMB { get; set; } = 1.44;
+
+ ///
+ /// Free space in MB (for display purposes).
+ ///
+ [JsonPropertyName("driveFreeMB")]
+ public double DriveFreeMB { get; set; } = 0.72;
+
+ ///
+ /// List of symlink entries to create in the drive root.
+ ///
+ [JsonPropertyName("paths")]
+ public List Paths { get; set; } = [];
+}
+
+///
+/// Represents a single symlink entry.
+///
+public class SymlinkEntry
+{
+ ///
+ /// Name of the symlink (folder/file name in the drive).
+ ///
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Target path the symlink points to.
+ ///
+ [JsonPropertyName("path")]
+ public string Path { get; set; } = string.Empty;
+}
diff --git a/DriveIconHelper.cs b/DriveIconHelper.cs
deleted file mode 100644
index 9f2ef9c..0000000
--- a/DriveIconHelper.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-using System.Runtime.Versioning;
-using Microsoft.Win32;
-
-namespace DevDrive;
-
-///
-/// Sets custom drive icons via the Windows registry.
-///
-[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);
- }
-}
diff --git a/FloppyConfig.cs b/FloppyConfig.cs
deleted file mode 100644
index 71ca47f..0000000
--- a/FloppyConfig.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System.Text.Json.Serialization;
-
-namespace DevDrive;
-
-///
-/// Represents the index.json configuration file structure.
-///
-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 Paths { get; set; } = [];
-}
-
-public class SymlinkEntry
-{
- [JsonPropertyName("name")]
- public string Name { get; set; } = string.Empty;
-
- [JsonPropertyName("path")]
- public string Path { get; set; } = string.Empty;
-}
diff --git a/FloppyFileNode.cs b/FloppyFileNode.cs
deleted file mode 100644
index b33c510..0000000
--- a/FloppyFileNode.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using System.Security.AccessControl;
-
-namespace DevDrive;
-
-///
-/// Represents a file or directory node in the virtual file system.
-///
-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;
-}
diff --git a/FloppyFileSystem.cs b/FloppyFileSystem.cs
deleted file mode 100644
index 088357d..0000000
--- a/FloppyFileSystem.cs
+++ /dev/null
@@ -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? 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 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 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 streams,
- IDokanFileInfo info)
- {
- streams = [];
- return DokanResult.NotImplemented;
- }
-}
diff --git a/FloppyStorage.cs b/FloppyStorage.cs
deleted file mode 100644
index 9d1cf0c..0000000
--- a/FloppyStorage.cs
+++ /dev/null
@@ -1,213 +0,0 @@
-using System.Collections.Concurrent;
-
-namespace DevDrive;
-
-///
-/// Manages the in-memory file system storage for the virtual drive.
-///
-public class FloppyStorage
-{
- public const long DefaultCapacity = 1_474_560;
- public const int SectorSize = 512;
- public const int AllocationUnitSize = 512;
-
- private readonly ConcurrentDictionary _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 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;
- }
-}
diff --git a/MirrorFileSystem.cs b/MirrorFileSystem.cs
new file mode 100644
index 0000000..493007b
--- /dev/null
+++ b/MirrorFileSystem.cs
@@ -0,0 +1,471 @@
+using System.Security.AccessControl;
+using DokanNet;
+using FileAccess = DokanNet.FileAccess;
+
+namespace DevDrive;
+
+///
+/// A Dokan filesystem that mirrors a real folder but reports custom capacity.
+///
+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;
+
+ ///
+ /// Creates a new mirror filesystem.
+ ///
+ /// The real folder to mirror.
+ /// The volume name to display.
+ /// Fake total capacity in bytes.
+ /// Fake free space in bytes.
+ public MirrorFileSystem(string rootPath, string volumeName, long totalBytes, long freeBytes)
+ {
+ _rootPath = rootPath;
+ _volumeName = volumeName;
+ _totalBytes = totalBytes;
+ _freeBytes = freeBytes;
+ }
+
+ ///
+ /// Updates the volume metadata (name and capacity) without remounting.
+ ///
+ /// New volume name.
+ /// New total capacity in bytes.
+ /// New free space in bytes.
+ 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 files, IDokanFileInfo info)
+ {
+ return FindFilesWithPattern(fileName, "*", out files, info);
+ }
+
+ public NtStatus FindFilesWithPattern(string fileName, string searchPattern, out IList files, IDokanFileInfo info)
+ {
+ files = new List();
+ 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 streams, IDokanFileInfo info)
+ {
+ streams = Array.Empty();
+ return DokanResult.NotImplemented;
+ }
+}
diff --git a/Program.cs b/Program.cs
index ee0730c..6cb9a82 100644
--- a/Program.cs
+++ b/Program.cs
@@ -7,53 +7,66 @@ using DokanNet.Logging;
namespace DevDrive;
///
-/// 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.
///
[SupportedOSPlatform("windows")]
public static class Program
{
- private const string DefaultMountPoint = "A:\\";
- private const string DefaultConfigDrive = "C:\\PhysicalDrive\\";
+ private const string DefaultDriveLetter = "A:";
+ private const string DefaultConfigPath = "C:\\PhysicalDrive\\";
private const string ConfigFile = "index.json";
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 DokanInstance? _dokanInstance;
- private static readonly ManualResetEvent ShutdownEvent = new(false);
+ private static MirrorFileSystem? _mirrorFs;
private static bool _isMounted;
- private static string _configDrive = DefaultConfigDrive;
- private static string _configFilePath = "";
- private static string _mountPoint = DefaultMountPoint;
- private static FloppyConfig? _lastConfig;
- private static readonly object MountLock = new();
+ private static bool _isUpdatingDevConfig;
private static bool _isConfigDriveLetter;
///
/// Main entry point.
///
- /// Command line arguments. [0] = mount point, [1] = config drive.
+ /// Command line arguments. [0] = drive letter (e.g. "A:"), [1] = config path.
public static void Main(string[] args)
{
- _mountPoint = args.Length > 0 ? args[0] : DefaultMountPoint;
- _configDrive = args.Length > 1 ? args[1] : DefaultConfigDrive;
+ _driveLetter = args.Length > 0 ? args[0].TrimEnd('\\') : DefaultDriveLetter;
+ _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);
- _isConfigDriveLetter = _configDrive.Length <= 3 && _configDrive[1] == ':';
+ var exeDir = AppContext.BaseDirectory;
+ _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} ==================");
- Console.WriteLine($"{LogPrefix} Watching for config drive: {_configDrive}");
+ Console.WriteLine($"{LogPrefix} DevDrive (Dokan + mklink mode)");
+ Console.WriteLine($"{LogPrefix} ==============================");
+ Console.WriteLine($"{LogPrefix} Drive letter: {_driveLetter}");
Console.WriteLine($"{LogPrefix} Config file: {_configFilePath}");
- Console.WriteLine($"{LogPrefix} Mount point: {_mountPoint}");
+ Console.WriteLine($"{LogPrefix} Root folder: {_devDrivePath}");
Console.WriteLine();
Console.WriteLine($"{LogPrefix} Press Ctrl+C to exit.");
Console.WriteLine();
@@ -68,18 +81,20 @@ public static class Program
try
{
_dokan = new Dokan(new ConsoleLogger(LogPrefix));
-
+ EnsureRootFolder();
+ SetupDevConfigWatcher();
+
if (_isConfigDriveLetter)
{
StartWmiWatchers();
}
else
{
- StartFileSystemWatcher();
+ StartVolumeWatchers();
}
-
+
CheckAndMountIfReady();
-
+
ShutdownEvent.WaitOne();
}
catch (Exception ex)
@@ -89,17 +104,54 @@ public static class Program
}
finally
{
- UnmountFloppy();
+ CleanupSymlinks();
+ UnmountDrive();
+ _devConfigWatcher?.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}");
- Console.WriteLine($"{LogPrefix} Watching for: {ConfigFile}");
+ private static bool IsConfigAccessible()
+ {
+ 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()
@@ -125,17 +177,19 @@ public static class Program
removeWatcher.EventArrived += (s, e) =>
{
Console.WriteLine($"{LogPrefix} Volume removed");
- lock (MountLock)
+ lock (SyncLock)
{
if (_isMounted && !IsConfigAccessible())
{
Console.WriteLine($"{LogPrefix} Config drive no longer accessible, unmounting...");
- UnmountFloppy();
+ UnmountDrive();
_lastConfig = null;
}
}
};
removeWatcher.Start();
+
+ Console.WriteLine($"{LogPrefix} Volume watchers started for {_configPath}");
}
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)
{
try
@@ -196,12 +210,12 @@ public static class Program
}
var insertedDrive = deviceId + "\\";
-
- if (string.Equals(insertedDrive, _configDrive, StringComparison.OrdinalIgnoreCase))
+
+ if (string.Equals(insertedDrive, _configPath, StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine();
Console.WriteLine($"{LogPrefix} Drive {deviceId} inserted!");
-
+
Thread.Sleep(500);
CheckAndMountIfReady();
}
@@ -225,18 +239,18 @@ public static class Program
}
var removedDrive = deviceId + "\\";
-
- if (string.Equals(removedDrive, _configDrive, StringComparison.OrdinalIgnoreCase))
+
+ if (string.Equals(removedDrive, _configPath, StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine();
Console.WriteLine($"{LogPrefix} Drive {deviceId} removed!");
-
- lock (MountLock)
+
+ lock (SyncLock)
{
if (_isMounted)
{
Console.WriteLine($"{LogPrefix} Config drive disconnected, unmounting...");
- UnmountFloppy();
+ UnmountDrive();
_lastConfig = null;
}
}
@@ -250,14 +264,14 @@ public static class Program
private static void CheckAndMountIfReady()
{
- lock (MountLock)
+ lock (SyncLock)
{
- var driveExists = Directory.Exists(_configDrive);
+ var driveExists = Directory.Exists(_configPath);
var configExists = driveExists && File.Exists(_configFilePath);
if (!driveExists)
{
- Console.WriteLine($"{LogPrefix} Waiting for {_configDrive} to be connected...");
+ Console.WriteLine($"{LogPrefix} Waiting for {_configPath} to be connected...");
return;
}
@@ -272,114 +286,55 @@ public static class Program
if (config == null || config.Paths.Count == 0)
{
Console.WriteLine($"{LogPrefix} Config empty or invalid");
-
+
if (_isMounted)
{
- UnmountFloppy();
+ UnmountDrive();
_lastConfig = null;
}
return;
}
- var configChanged = !ConfigEquals(_lastConfig, config);
-
- if (!_isMounted || configChanged)
+ if (!_isMounted)
{
- if (_isMounted && configChanged)
- {
- Console.WriteLine($"{LogPrefix} Config changed, remounting...");
- UnmountFloppy();
- }
-
- MountFloppy(config);
+ EnsureDevConfigFromConfig(config);
+ MountDrive();
+ SyncSymlinksFromConfig(config);
_lastConfig = config;
}
}
}
- private static FloppyConfig? ReadConfig(string configFilePath)
+ private static void MountDrive()
{
- try
- {
- var json = File.ReadAllText(configFilePath);
- return JsonSerializer.Deserialize(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)
+ if (_dokan == null || _isMounted)
{
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
{
- var symlinks = new Dictionary();
- 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);
+ _mirrorFs = new MirrorFileSystem(_devDrivePath, volumeName, totalBytes, freeBytes);
var dokanBuilder = new DokanInstanceBuilder(_dokan)
.ConfigureOptions(options =>
{
- options.Options = DokanOptions.MountManager | DokanOptions.CurrentSession;
+ options.Options = DokanOptions.FixedDrive | DokanOptions.CurrentSession;
options.MountPoint = _mountPoint;
options.SingleThread = false;
- options.AllocationUnitSize = FloppyStorage.AllocationUnitSize;
- options.SectorSize = FloppyStorage.SectorSize;
});
- _dokanInstance = dokanBuilder.Build(floppyFs);
+ _dokanInstance = dokanBuilder.Build(_mirrorFs);
_isMounted = true;
- Console.WriteLine($"{LogPrefix} Mounted at {_mountPoint}");
-
- var iconPath = string.IsNullOrWhiteSpace(config.DriveIcon)
- ? "%SystemRoot%\\System32\\shell32.dll,6"
- : config.DriveIcon;
- DriveIconHelper.SetDriveIcon(_mountPoint, iconPath, config.DriveName);
+ Console.WriteLine($"{LogPrefix} Mounted {_driveLetter} (Dokan)");
+ Console.WriteLine($"{LogPrefix} Volume: {volumeName}");
+ Console.WriteLine($"{LogPrefix} Capacity: {config?.DriveTotalMB ?? 1.44} MB total, {config?.DriveFreeMB ?? 0.72} MB free");
}
catch (DokanException ex)
{
@@ -388,7 +343,7 @@ public static class Program
}
}
- private static void UnmountFloppy()
+ private static void UnmountDrive()
{
if (!_isMounted)
{
@@ -397,16 +352,287 @@ public static class Program
try
{
- DriveIconHelper.RemoveDriveIcon();
_dokan?.RemoveMountPoint(_mountPoint);
_dokanInstance?.Dispose();
_dokanInstance = null;
+ _mirrorFs = null;
_isMounted = false;
- Console.WriteLine($"{LogPrefix} Unmounted");
+ Console.WriteLine($"{LogPrefix} Unmounted {_driveLetter}");
}
catch (Exception ex)
{
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(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(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());
+ }
}
diff --git a/SymlinkHandler.cs b/SymlinkHandler.cs
deleted file mode 100644
index 0b21381..0000000
--- a/SymlinkHandler.cs
+++ /dev/null
@@ -1,201 +0,0 @@
-using DokanNet;
-
-namespace DevDrive;
-
-///
-/// Handles passthrough operations for symlinks to real file system paths.
-///
-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 ListRealDirectory(string realPath)
- {
- var files = new List();
-
- 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;
- }
- }
-}