Initial import: renamed to DevDrive
This commit is contained in:
48
.gitignore
vendored
Normal file
48
.gitignore
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
# .NET / Visual Studio build outputs
|
||||
.vs/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
|
||||
# VS Code workspace settings
|
||||
.vscode/
|
||||
|
||||
# NuGet packages and artifacts
|
||||
*.nupkg
|
||||
packages/
|
||||
|
||||
# Test results and coverage
|
||||
TestResult*/
|
||||
*.TestResult.xml
|
||||
coverage/
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# User-specific and IDE files
|
||||
*.user
|
||||
*.suo
|
||||
*.userprefs
|
||||
.idea/
|
||||
*.sln.iml
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
|
||||
# Logs and caches
|
||||
*.log
|
||||
*.svclog
|
||||
*.pidb
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
|
||||
# Debug symbols
|
||||
*.pdb
|
||||
|
||||
# OS-specific clutter
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
Desktop.ini
|
||||
.DS_Store
|
||||
|
||||
# Publish outputs
|
||||
publish/
|
||||
**/PublishOutput/
|
||||
16
DevDrive/DevDrive.csproj
Normal file
16
DevDrive/DevDrive.csproj
Normal file
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<PlatformTarget>x64</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DokanNet" Version="2.3.0.3" />
|
||||
<PackageReference Include="System.Management" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
174
DevDrive/DriveHider.cs
Normal file
174
DevDrive/DriveHider.cs
Normal file
@@ -0,0 +1,174 @@
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace DevDrive;
|
||||
|
||||
/// <summary>
|
||||
/// Handles hiding and showing drives from Windows Explorer using registry.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static class DriveHider
|
||||
{
|
||||
private const string LogPrefix = "[DriveHider]";
|
||||
private const string ExplorerPoliciesKey = @"Software\Microsoft\Windows\CurrentVersion\Policies\Explorer";
|
||||
private const string NoDrivesValue = "NoDrives";
|
||||
|
||||
private static int _hiddenDriveMask;
|
||||
private static string? _hiddenDriveLetter;
|
||||
private static int _originalNoDrives;
|
||||
|
||||
public static bool HideDrive(string driveLetter)
|
||||
{
|
||||
if (_hiddenDriveLetter != null)
|
||||
{
|
||||
Console.WriteLine($"{LogPrefix} Drive already hidden");
|
||||
return true;
|
||||
}
|
||||
|
||||
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 driveIndex = letter[0] - 'A';
|
||||
_hiddenDriveMask = 1 << driveIndex;
|
||||
_hiddenDriveLetter = letter + ":\\";
|
||||
|
||||
using var key = Registry.CurrentUser.CreateSubKey(ExplorerPoliciesKey);
|
||||
if (key == null)
|
||||
{
|
||||
Console.WriteLine($"{LogPrefix} Failed to open registry key");
|
||||
return false;
|
||||
}
|
||||
|
||||
_originalNoDrives = GetNoDrivesValue(key);
|
||||
var newValue = _originalNoDrives | _hiddenDriveMask;
|
||||
|
||||
key.SetValue(NoDrivesValue, newValue, RegistryValueKind.DWord);
|
||||
|
||||
RefreshExplorer();
|
||||
|
||||
Console.WriteLine($"{LogPrefix} Hidden drive {letter}: from Explorer");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"{LogPrefix} Error hiding drive: {ex.Message}");
|
||||
_hiddenDriveMask = 0;
|
||||
_hiddenDriveLetter = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool ShowDrive()
|
||||
{
|
||||
if (_hiddenDriveLetter == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey(ExplorerPoliciesKey, writable: true);
|
||||
if (key == null)
|
||||
{
|
||||
Console.WriteLine($"{LogPrefix} Failed to open registry key for restore");
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentValue = GetNoDrivesValue(key);
|
||||
var newValue = currentValue & ~_hiddenDriveMask;
|
||||
|
||||
if (newValue == 0)
|
||||
{
|
||||
key.DeleteValue(NoDrivesValue, throwOnMissingValue: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
key.SetValue(NoDrivesValue, newValue, RegistryValueKind.DWord);
|
||||
}
|
||||
|
||||
RefreshExplorer();
|
||||
|
||||
Console.WriteLine($"{LogPrefix} Restored drive {_hiddenDriveLetter.TrimEnd('\\')} in Explorer");
|
||||
_hiddenDriveMask = 0;
|
||||
_hiddenDriveLetter = null;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"{LogPrefix} Error restoring drive: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsHidden => _hiddenDriveLetter != null;
|
||||
public static string? HiddenDriveLetter => _hiddenDriveLetter;
|
||||
|
||||
private static int GetNoDrivesValue(RegistryKey key)
|
||||
{
|
||||
var value = key.GetValue(NoDrivesValue);
|
||||
|
||||
return value switch
|
||||
{
|
||||
null => 0,
|
||||
int intValue => intValue,
|
||||
uint uintValue => (int)uintValue,
|
||||
byte[] bytes when bytes.Length >= 4 => BitConverter.ToInt32(bytes, 0),
|
||||
byte[] bytes when bytes.Length > 0 => bytes[0],
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
|
||||
private static void RefreshExplorer()
|
||||
{
|
||||
try
|
||||
{
|
||||
NativeMethods.SendMessageTimeout(
|
||||
NativeMethods.HWND_BROADCAST,
|
||||
NativeMethods.WM_SETTINGCHANGE,
|
||||
IntPtr.Zero,
|
||||
"Policy",
|
||||
NativeMethods.SMTO_ABORTIFHUNG,
|
||||
1000,
|
||||
out _);
|
||||
|
||||
NativeMethods.SHChangeNotify(
|
||||
NativeMethods.SHCNE_DRIVEADD | NativeMethods.SHCNE_DRIVEREMOVED,
|
||||
NativeMethods.SHCNF_FLUSHNOWAIT,
|
||||
IntPtr.Zero,
|
||||
IntPtr.Zero);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static class NativeMethods
|
||||
{
|
||||
public const int SHCNE_DRIVEADD = 0x00000100;
|
||||
public const int SHCNE_DRIVEREMOVED = 0x00000080;
|
||||
public const int SHCNF_FLUSHNOWAIT = 0x3000;
|
||||
|
||||
public static readonly IntPtr HWND_BROADCAST = new(0xFFFF);
|
||||
public const int WM_SETTINGCHANGE = 0x001A;
|
||||
public const int SMTO_ABORTIFHUNG = 0x0002;
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("shell32.dll")]
|
||||
public static extern void SHChangeNotify(int wEventId, int uFlags, IntPtr dwItem1, IntPtr dwItem2);
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
|
||||
public static extern IntPtr SendMessageTimeout(
|
||||
IntPtr hWnd,
|
||||
int Msg,
|
||||
IntPtr wParam,
|
||||
string lParam,
|
||||
int fuFlags,
|
||||
int uTimeout,
|
||||
out IntPtr lpdwResult);
|
||||
}
|
||||
}
|
||||
105
DevDrive/DriveIconHelper.cs
Normal file
105
DevDrive/DriveIconHelper.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
33
DevDrive/FloppyConfig.cs
Normal file
33
DevDrive/FloppyConfig.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
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;
|
||||
}
|
||||
39
DevDrive/FloppyFileNode.cs
Normal file
39
DevDrive/FloppyFileNode.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
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;
|
||||
}
|
||||
631
DevDrive/FloppyFileSystem.cs
Normal file
631
DevDrive/FloppyFileSystem.cs
Normal file
@@ -0,0 +1,631 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
213
DevDrive/FloppyStorage.cs
Normal file
213
DevDrive/FloppyStorage.cs
Normal file
@@ -0,0 +1,213 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
413
DevDrive/Program.cs
Normal file
413
DevDrive/Program.cs
Normal file
@@ -0,0 +1,413 @@
|
||||
using System.Management;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text.Json;
|
||||
using DokanNet;
|
||||
using DokanNet.Logging;
|
||||
|
||||
namespace DevDrive;
|
||||
|
||||
/// <summary>
|
||||
/// Entry point for the virtual drive application.
|
||||
/// Monitors a config drive via WMI events and creates symlinks based on index.json.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static class Program
|
||||
{
|
||||
private const string DefaultMountPoint = "A:\\";
|
||||
private const string DefaultConfigDrive = "C:\\PhysicalDrive\\";
|
||||
private const string ConfigFile = "index.json";
|
||||
private const string LogPrefix = "[DevDrive]";
|
||||
|
||||
private static Dokan? _dokan;
|
||||
private static DokanInstance? _dokanInstance;
|
||||
private static readonly ManualResetEvent ShutdownEvent = new(false);
|
||||
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 _isConfigDriveLetter;
|
||||
|
||||
/// <summary>
|
||||
/// Main entry point.
|
||||
/// </summary>
|
||||
/// <param name="args">Command line arguments. [0] = mount point, [1] = config drive.</param>
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
_mountPoint = args.Length > 0 ? args[0] : DefaultMountPoint;
|
||||
_configDrive = args.Length > 1 ? args[1] : DefaultConfigDrive;
|
||||
|
||||
if (!_mountPoint.EndsWith("\\"))
|
||||
{
|
||||
_mountPoint += "\\";
|
||||
}
|
||||
if (!_configDrive.EndsWith("\\"))
|
||||
{
|
||||
_configDrive += "\\";
|
||||
}
|
||||
|
||||
_configFilePath = Path.Combine(_configDrive, ConfigFile);
|
||||
_isConfigDriveLetter = _configDrive.Length <= 3 && _configDrive[1] == ':';
|
||||
|
||||
Console.WriteLine($"{LogPrefix} DevDrive");
|
||||
Console.WriteLine($"{LogPrefix} ==================");
|
||||
Console.WriteLine($"{LogPrefix} Watching for config drive: {_configDrive}");
|
||||
Console.WriteLine($"{LogPrefix} Config file: {_configFilePath}");
|
||||
Console.WriteLine($"{LogPrefix} Mount point: {_mountPoint}");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"{LogPrefix} Press Ctrl+C to exit.");
|
||||
Console.WriteLine();
|
||||
|
||||
Console.CancelKeyPress += (sender, eventArgs) =>
|
||||
{
|
||||
eventArgs.Cancel = true;
|
||||
Console.WriteLine($"{LogPrefix} Shutting down...");
|
||||
ShutdownEvent.Set();
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
_dokan = new Dokan(new ConsoleLogger(LogPrefix));
|
||||
|
||||
if (_isConfigDriveLetter)
|
||||
{
|
||||
StartWmiWatchers();
|
||||
}
|
||||
else
|
||||
{
|
||||
StartFileSystemWatcher();
|
||||
}
|
||||
|
||||
CheckAndMountIfReady();
|
||||
|
||||
ShutdownEvent.WaitOne();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"{LogPrefix} Fatal error: {ex.Message}");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
UnmountFloppy();
|
||||
_dokan?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static void StartFileSystemWatcher()
|
||||
{
|
||||
StartVolumeWatchers();
|
||||
|
||||
Console.WriteLine($"{LogPrefix} Volume watchers started for {_configDrive}");
|
||||
Console.WriteLine($"{LogPrefix} Watching for: {ConfigFile}");
|
||||
}
|
||||
|
||||
private static void StartVolumeWatchers()
|
||||
{
|
||||
try
|
||||
{
|
||||
var insertQuery = new WqlEventQuery(
|
||||
"SELECT * FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Volume'"
|
||||
);
|
||||
var insertWatcher = new ManagementEventWatcher(insertQuery);
|
||||
insertWatcher.EventArrived += (s, e) =>
|
||||
{
|
||||
Console.WriteLine($"{LogPrefix} Volume added, checking config...");
|
||||
Thread.Sleep(1000);
|
||||
CheckAndMountIfReady();
|
||||
};
|
||||
insertWatcher.Start();
|
||||
|
||||
var removeQuery = new WqlEventQuery(
|
||||
"SELECT * FROM __InstanceDeletionEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Volume'"
|
||||
);
|
||||
var removeWatcher = new ManagementEventWatcher(removeQuery);
|
||||
removeWatcher.EventArrived += (s, e) =>
|
||||
{
|
||||
Console.WriteLine($"{LogPrefix} Volume removed");
|
||||
lock (MountLock)
|
||||
{
|
||||
if (_isMounted && !IsConfigAccessible())
|
||||
{
|
||||
Console.WriteLine($"{LogPrefix} Config drive no longer accessible, unmounting...");
|
||||
UnmountFloppy();
|
||||
_lastConfig = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
removeWatcher.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"{LogPrefix} Warning: Could not start WMI volume watchers: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
var targetInstance = (ManagementBaseObject)e.NewEvent["TargetInstance"];
|
||||
var deviceId = targetInstance["DeviceID"]?.ToString();
|
||||
|
||||
if (string.IsNullOrEmpty(deviceId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var insertedDrive = deviceId + "\\";
|
||||
|
||||
if (string.Equals(insertedDrive, _configDrive, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"{LogPrefix} Drive {deviceId} inserted!");
|
||||
|
||||
Thread.Sleep(500);
|
||||
CheckAndMountIfReady();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"{LogPrefix} Error handling drive insert: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnDriveRemoved(object sender, EventArrivedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var targetInstance = (ManagementBaseObject)e.NewEvent["TargetInstance"];
|
||||
var deviceId = targetInstance["DeviceID"]?.ToString();
|
||||
|
||||
if (string.IsNullOrEmpty(deviceId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var removedDrive = deviceId + "\\";
|
||||
|
||||
if (string.Equals(removedDrive, _configDrive, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"{LogPrefix} Drive {deviceId} removed!");
|
||||
|
||||
lock (MountLock)
|
||||
{
|
||||
if (_isMounted)
|
||||
{
|
||||
Console.WriteLine($"{LogPrefix} Config drive disconnected, unmounting...");
|
||||
UnmountFloppy();
|
||||
_lastConfig = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"{LogPrefix} Error handling drive removal: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void CheckAndMountIfReady()
|
||||
{
|
||||
lock (MountLock)
|
||||
{
|
||||
var driveExists = Directory.Exists(_configDrive);
|
||||
var configExists = driveExists && File.Exists(_configFilePath);
|
||||
|
||||
if (!driveExists)
|
||||
{
|
||||
Console.WriteLine($"{LogPrefix} Waiting for {_configDrive} to be connected...");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!configExists)
|
||||
{
|
||||
Console.WriteLine($"{LogPrefix} Drive present but {ConfigFile} not found");
|
||||
return;
|
||||
}
|
||||
|
||||
var config = ReadConfig(_configFilePath);
|
||||
|
||||
if (config == null || config.Paths.Count == 0)
|
||||
{
|
||||
Console.WriteLine($"{LogPrefix} Config empty or invalid");
|
||||
|
||||
if (_isMounted)
|
||||
{
|
||||
UnmountFloppy();
|
||||
_lastConfig = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var configChanged = !ConfigEquals(_lastConfig, config);
|
||||
|
||||
if (!_isMounted || configChanged)
|
||||
{
|
||||
if (_isMounted && configChanged)
|
||||
{
|
||||
Console.WriteLine($"{LogPrefix} Config changed, remounting...");
|
||||
UnmountFloppy();
|
||||
}
|
||||
|
||||
MountFloppy(config);
|
||||
_lastConfig = config;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static FloppyConfig? ReadConfig(string configFilePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var symlinks = new Dictionary<string, string>();
|
||||
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)
|
||||
.ConfigureOptions(options =>
|
||||
{
|
||||
options.Options = DokanOptions.MountManager | DokanOptions.CurrentSession;
|
||||
options.MountPoint = _mountPoint;
|
||||
options.SingleThread = false;
|
||||
options.AllocationUnitSize = FloppyStorage.AllocationUnitSize;
|
||||
options.SectorSize = FloppyStorage.SectorSize;
|
||||
});
|
||||
|
||||
_dokanInstance = dokanBuilder.Build(floppyFs);
|
||||
_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);
|
||||
}
|
||||
catch (DokanException ex)
|
||||
{
|
||||
Console.WriteLine($"{LogPrefix} Mount error: {ex.Message}");
|
||||
_isMounted = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void UnmountFloppy()
|
||||
{
|
||||
if (!_isMounted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
DriveIconHelper.RemoveDriveIcon();
|
||||
_dokan?.RemoveMountPoint(_mountPoint);
|
||||
_dokanInstance?.Dispose();
|
||||
_dokanInstance = null;
|
||||
_isMounted = false;
|
||||
Console.WriteLine($"{LogPrefix} Unmounted");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"{LogPrefix} Unmount error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
201
DevDrive/SymlinkHandler.cs
Normal file
201
DevDrive/SymlinkHandler.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user