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