diff --git a/DevDriveConfig.cs b/DevDriveConfig.cs new file mode 100644 index 0000000..f08a759 --- /dev/null +++ b/DevDriveConfig.cs @@ -0,0 +1,57 @@ +using System.Text.Json.Serialization; + +namespace DevDrive; + +/// +/// Represents the index.json configuration file structure. +/// +public class DevDriveConfig +{ + /// + /// Display name for the drive. + /// + [JsonPropertyName("driveName")] + public string DriveName { get; set; } = "Dev Drive"; + + /// + /// Icon path in format "path,index" (e.g. "%SystemRoot%\System32\shell32.dll,8"). + /// + [JsonPropertyName("driveIcon")] + public string DriveIcon { get; set; } = ""; + + /// + /// Total drive capacity in MB (for display purposes). + /// + [JsonPropertyName("driveTotalMB")] + public double DriveTotalMB { get; set; } = 1.44; + + /// + /// Free space in MB (for display purposes). + /// + [JsonPropertyName("driveFreeMB")] + public double DriveFreeMB { get; set; } = 0.72; + + /// + /// List of symlink entries to create in the drive root. + /// + [JsonPropertyName("paths")] + public List Paths { get; set; } = []; +} + +/// +/// Represents a single symlink entry. +/// +public class SymlinkEntry +{ + /// + /// Name of the symlink (folder/file name in the drive). + /// + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// + /// Target path the symlink points to. + /// + [JsonPropertyName("path")] + public string Path { get; set; } = string.Empty; +} diff --git a/DriveIconHelper.cs b/DriveIconHelper.cs deleted file mode 100644 index 9f2ef9c..0000000 --- a/DriveIconHelper.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Runtime.Versioning; -using Microsoft.Win32; - -namespace DevDrive; - -/// -/// Sets custom drive icons via the Windows registry. -/// -[SupportedOSPlatform("windows")] -public static class DriveIconHelper -{ - private const string LogPrefix = "[DriveIcon]"; - private const string DriveIconsKey = @"Software\Classes\Applications\Explorer.exe\Drives"; - - private static string? _currentDriveLetter; - - public static bool SetDriveIcon(string driveLetter, string iconPath, string? driveLabel = null) - { - try - { - var letter = driveLetter.TrimEnd('\\', ':').ToUpperInvariant(); - if (letter.Length != 1 || letter[0] < 'A' || letter[0] > 'Z') - { - Console.WriteLine($"{LogPrefix} Invalid drive letter: {driveLetter}"); - return false; - } - - var driveKeyPath = @$"{DriveIconsKey}\{letter}\DefaultIcon"; - - using var key = Registry.CurrentUser.CreateSubKey(driveKeyPath); - if (key == null) - { - Console.WriteLine($"{LogPrefix} Failed to create registry key"); - return false; - } - - key.SetValue("", iconPath); - _currentDriveLetter = letter; - - if (!string.IsNullOrEmpty(driveLabel)) - { - var labelKeyPath = @$"{DriveIconsKey}\{letter}\DefaultLabel"; - using var labelKey = Registry.CurrentUser.CreateSubKey(labelKeyPath); - labelKey?.SetValue("", driveLabel); - } - - RefreshShell(); - - Console.WriteLine($"{LogPrefix} Set icon for drive {letter}: to {iconPath}"); - return true; - } - catch (Exception ex) - { - Console.WriteLine($"{LogPrefix} Error setting icon: {ex.Message}"); - return false; - } - } - - public static void RemoveDriveIcon() - { - if (_currentDriveLetter == null) - { - return; - } - - try - { - var driveKeyPath = @$"{DriveIconsKey}\{_currentDriveLetter}"; - Registry.CurrentUser.DeleteSubKeyTree(driveKeyPath, throwOnMissingSubKey: false); - - Console.WriteLine($"{LogPrefix} Removed icon for drive {_currentDriveLetter}:"); - _currentDriveLetter = null; - - RefreshShell(); - } - catch (Exception ex) - { - Console.WriteLine($"{LogPrefix} Error removing icon: {ex.Message}"); - } - } - - private static void RefreshShell() - { - try - { - NativeMethods.SHChangeNotify( - NativeMethods.SHCNE_ASSOCCHANGED, - NativeMethods.SHCNF_IDLIST, - IntPtr.Zero, - IntPtr.Zero); - } - catch - { - } - } - - private static class NativeMethods - { - public const int SHCNE_ASSOCCHANGED = 0x08000000; - public const int SHCNF_IDLIST = 0x0000; - - [System.Runtime.InteropServices.DllImport("shell32.dll")] - public static extern void SHChangeNotify(int wEventId, int uFlags, IntPtr dwItem1, IntPtr dwItem2); - } -} diff --git a/FloppyConfig.cs b/FloppyConfig.cs deleted file mode 100644 index 71ca47f..0000000 --- a/FloppyConfig.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Text.Json.Serialization; - -namespace DevDrive; - -/// -/// Represents the index.json configuration file structure. -/// -public class FloppyConfig -{ - [JsonPropertyName("driveName")] - public string DriveName { get; set; } = "Dev Disk"; - - [JsonPropertyName("driveIcon")] - public string DriveIcon { get; set; } = ""; - - [JsonPropertyName("driveTotalMB")] - public double DriveTotalMB { get; set; } = 100; - - [JsonPropertyName("driveFreeMB")] - public double DriveFreeMB { get; set; } = 50; - - [JsonPropertyName("paths")] - public List Paths { get; set; } = []; -} - -public class SymlinkEntry -{ - [JsonPropertyName("name")] - public string Name { get; set; } = string.Empty; - - [JsonPropertyName("path")] - public string Path { get; set; } = string.Empty; -} diff --git a/FloppyFileNode.cs b/FloppyFileNode.cs deleted file mode 100644 index b33c510..0000000 --- a/FloppyFileNode.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Security.AccessControl; - -namespace DevDrive; - -/// -/// Represents a file or directory node in the virtual file system. -/// -public class FloppyFileNode -{ - public string Name { get; set; } - public string FullPath { get; set; } - public bool IsDirectory { get; set; } - public FileAttributes Attributes { get; set; } - public byte[] Data { get; set; } - public DateTime CreationTime { get; set; } - public DateTime LastAccessTime { get; set; } - public DateTime LastWriteTime { get; set; } - public FileSystemSecurity? Security { get; set; } - public string? SymlinkTarget { get; set; } - public bool IsSymlink => !string.IsNullOrEmpty(SymlinkTarget); - - public FloppyFileNode(string fullPath, bool isDirectory) - { - FullPath = fullPath.TrimEnd('\\'); - Name = Path.GetFileName(fullPath); - if (string.IsNullOrEmpty(Name)) - { - Name = "\\"; - } - IsDirectory = isDirectory; - Attributes = isDirectory ? FileAttributes.Directory : FileAttributes.Normal; - Data = []; - CreationTime = DateTime.Now; - LastAccessTime = DateTime.Now; - LastWriteTime = DateTime.Now; - } - - public long FileSize => IsDirectory ? 0 : Data.Length; -} diff --git a/FloppyFileSystem.cs b/FloppyFileSystem.cs deleted file mode 100644 index 088357d..0000000 --- a/FloppyFileSystem.cs +++ /dev/null @@ -1,631 +0,0 @@ -using System.Security.AccessControl; -using DokanNet; -using FileAccess = DokanNet.FileAccess; - -namespace DevDrive; - -public class FloppyFileSystem : IDokanOperations -{ - private const string DefaultVolumeName = "Dev Disk"; - private const string FileSystemName = "FAT"; - private const uint VolumeSerial = 0x19950101; - - private readonly FloppyStorage _storage; - private readonly string _volumeName; - - public FloppyFileSystem(string? volumeName = null, long totalBytes = FloppyStorage.DefaultCapacity, long freeBytes = FloppyStorage.DefaultCapacity) - { - _storage = new FloppyStorage(totalBytes, freeBytes); - _volumeName = volumeName ?? DefaultVolumeName; - } - - public FloppyFileSystem(Dictionary? symlinks, string? volumeName, long totalBytes, long freeBytes) - { - _storage = new FloppyStorage(totalBytes, freeBytes); - _volumeName = volumeName ?? DefaultVolumeName; - - if (symlinks != null) - { - foreach (var (path, target) in symlinks) - { - _storage.CreateSymlink(path, target, true); - } - } - } - - public NtStatus CreateFile( - string fileName, - FileAccess access, - FileShare share, - FileMode mode, - FileOptions options, - FileAttributes attributes, - IDokanFileInfo info) - { - var path = FloppyStorage.NormalizePath(fileName); - - if (SymlinkHandler.TryResolveSymlink(path, _storage, out var realPath) && realPath != null) - { - return SymlinkHandler.CreateRealFileOrDirectory(realPath, info.IsDirectory, mode); - } - - var node = _storage.GetNode(path); - var parentPath = FloppyStorage.GetParentPath(path); - var parentExists = _storage.Exists(parentPath); - - if (info.IsDirectory) - { - switch (mode) - { - case FileMode.CreateNew: - if (node != null) - { - return DokanResult.FileExists; - } - if (!parentExists) - { - return DokanResult.PathNotFound; - } - var newDir = _storage.CreateNode(path, true); - if (newDir == null) - { - return DokanResult.Error; - } - return DokanResult.Success; - - case FileMode.Open: - if (node == null) - { - return DokanResult.PathNotFound; - } - if (!node.IsDirectory) - { - return DokanResult.NotADirectory; - } - return DokanResult.Success; - - default: - return DokanResult.Success; - } - } - - var pathExists = node != null; - var pathIsDirectory = node?.IsDirectory ?? false; - - switch (mode) - { - case FileMode.Open: - if (!pathExists) - { - return DokanResult.FileNotFound; - } - if (pathIsDirectory) - { - info.IsDirectory = true; - } - break; - - case FileMode.CreateNew: - if (pathExists) - { - return DokanResult.FileExists; - } - if (!parentExists) - { - return DokanResult.PathNotFound; - } - _storage.CreateNode(path, false); - break; - - case FileMode.Create: - if (pathExists && pathIsDirectory) - { - return DokanResult.AccessDenied; - } - if (!parentExists) - { - return DokanResult.PathNotFound; - } - if (pathExists) - { - var existing = _storage.GetNode(path); - if (existing != null) - { - existing.Data = []; - existing.LastWriteTime = DateTime.Now; - } - } - else - { - _storage.CreateNode(path, false); - } - break; - - case FileMode.OpenOrCreate: - if (!pathExists) - { - if (!parentExists) - { - return DokanResult.PathNotFound; - } - _storage.CreateNode(path, false); - } - else if (pathIsDirectory) - { - info.IsDirectory = true; - } - break; - - case FileMode.Truncate: - if (!pathExists) - { - return DokanResult.FileNotFound; - } - if (pathIsDirectory) - { - return DokanResult.AccessDenied; - } - var truncNode = _storage.GetNode(path); - if (truncNode != null) - { - truncNode.Data = []; - truncNode.LastWriteTime = DateTime.Now; - } - break; - - case FileMode.Append: - if (!pathExists) - { - if (!parentExists) - { - return DokanResult.PathNotFound; - } - _storage.CreateNode(path, false); - } - break; - } - - return DokanResult.Success; - } - - public void Cleanup(string fileName, IDokanFileInfo info) - { - if (info.IsDirectory && _storage.Exists(FloppyStorage.NormalizePath(fileName))) - { - return; - } - } - - public void CloseFile(string fileName, IDokanFileInfo info) - { - } - - public NtStatus ReadFile( - string fileName, - byte[] buffer, - out int bytesRead, - long offset, - IDokanFileInfo info) - { - var path = FloppyStorage.NormalizePath(fileName); - - if (SymlinkHandler.TryResolveSymlink(path, _storage, out var realPath) && realPath != null) - { - return SymlinkHandler.ReadRealFile(realPath, buffer, out bytesRead, offset); - } - - var node = _storage.GetNode(path); - - if (node == null || node.IsDirectory) - { - bytesRead = 0; - return DokanResult.FileNotFound; - } - - node.LastAccessTime = DateTime.Now; - - if (offset >= node.Data.Length) - { - bytesRead = 0; - return DokanResult.Success; - } - - var toRead = Math.Min(buffer.Length, (int)(node.Data.Length - offset)); - Array.Copy(node.Data, offset, buffer, 0, toRead); - bytesRead = toRead; - - return DokanResult.Success; - } - - public NtStatus WriteFile( - string fileName, - byte[] buffer, - out int bytesWritten, - long offset, - IDokanFileInfo info) - { - var path = FloppyStorage.NormalizePath(fileName); - - if (SymlinkHandler.TryResolveSymlink(path, _storage, out var realPath) && realPath != null) - { - return SymlinkHandler.WriteRealFile(realPath, buffer, out bytesWritten, offset); - } - - var node = _storage.GetNode(path); - - if (node == null || node.IsDirectory) - { - bytesWritten = 0; - return DokanResult.FileNotFound; - } - - if (info.PagingIo) - { - if (offset >= node.Data.Length) - { - bytesWritten = 0; - return DokanResult.Success; - } - var toWrite = Math.Min(buffer.Length, (int)(node.Data.Length - offset)); - Array.Copy(buffer, 0, node.Data, offset, toWrite); - bytesWritten = toWrite; - return DokanResult.Success; - } - - var newLength = offset + buffer.Length; - var additionalBytes = newLength - node.Data.Length; - - if (additionalBytes > 0 && !_storage.HasEnoughSpace(additionalBytes)) - { - bytesWritten = 0; - return DokanResult.DiskFull; - } - - if (newLength > node.Data.Length) - { - var newData = new byte[newLength]; - Array.Copy(node.Data, newData, node.Data.Length); - node.Data = newData; - } - - Array.Copy(buffer, 0, node.Data, offset, buffer.Length); - bytesWritten = buffer.Length; - node.LastWriteTime = DateTime.Now; - - return DokanResult.Success; - } - - public NtStatus FlushFileBuffers(string fileName, IDokanFileInfo info) - { - return DokanResult.Success; - } - - public NtStatus GetFileInformation( - string fileName, - out FileInformation fileInfo, - IDokanFileInfo info) - { - var path = FloppyStorage.NormalizePath(fileName); - - if (SymlinkHandler.TryResolveSymlink(path, _storage, out var realPath) && realPath != null) - { - if (SymlinkHandler.GetRealFileInfo(realPath, out fileInfo)) - { - return DokanResult.Success; - } - return DokanResult.FileNotFound; - } - - var node = _storage.GetNode(path); - - if (node == null) - { - fileInfo = default; - return DokanResult.FileNotFound; - } - - fileInfo = new FileInformation - { - FileName = node.Name, - Attributes = node.Attributes, - CreationTime = node.CreationTime, - LastAccessTime = node.LastAccessTime, - LastWriteTime = node.LastWriteTime, - Length = node.FileSize - }; - - return DokanResult.Success; - } - - public NtStatus FindFiles( - string fileName, - out IList files, - IDokanFileInfo info) - { - var path = FloppyStorage.NormalizePath(fileName); - - if (SymlinkHandler.TryResolveSymlink(path, _storage, out var realPath) && realPath != null) - { - files = SymlinkHandler.ListRealDirectory(realPath); - return DokanResult.Success; - } - - var children = _storage.GetChildren(path); - - files = children.Select(node => new FileInformation - { - FileName = node.Name, - Attributes = node.Attributes, - CreationTime = node.CreationTime, - LastAccessTime = node.LastAccessTime, - LastWriteTime = node.LastWriteTime, - Length = node.FileSize - }).ToList(); - - return DokanResult.Success; - } - - public NtStatus FindFilesWithPattern( - string fileName, - string searchPattern, - out IList files, - IDokanFileInfo info) - { - files = []; - return DokanResult.NotImplemented; - } - - public NtStatus SetFileAttributes( - string fileName, - FileAttributes attributes, - IDokanFileInfo info) - { - var path = FloppyStorage.NormalizePath(fileName); - var node = _storage.GetNode(path); - - if (node == null) - { - return DokanResult.FileNotFound; - } - - node.Attributes = attributes; - return DokanResult.Success; - } - - public NtStatus SetFileTime( - string fileName, - DateTime? creationTime, - DateTime? lastAccessTime, - DateTime? lastWriteTime, - IDokanFileInfo info) - { - var path = FloppyStorage.NormalizePath(fileName); - var node = _storage.GetNode(path); - - if (node == null) - { - return DokanResult.FileNotFound; - } - - if (creationTime.HasValue) - { - node.CreationTime = creationTime.Value; - } - if (lastAccessTime.HasValue) - { - node.LastAccessTime = lastAccessTime.Value; - } - if (lastWriteTime.HasValue) - { - node.LastWriteTime = lastWriteTime.Value; - } - - return DokanResult.Success; - } - - public NtStatus DeleteFile(string fileName, IDokanFileInfo info) - { - var path = FloppyStorage.NormalizePath(fileName); - var node = _storage.GetNode(path); - - if (node == null) - { - return DokanResult.FileNotFound; - } - - if (node.IsDirectory) - { - return DokanResult.AccessDenied; - } - - return DokanResult.Success; - } - - public NtStatus DeleteDirectory(string fileName, IDokanFileInfo info) - { - var path = FloppyStorage.NormalizePath(fileName); - var node = _storage.GetNode(path); - - if (node == null) - { - return DokanResult.PathNotFound; - } - - if (!node.IsDirectory) - { - return DokanResult.NotADirectory; - } - - var children = _storage.GetChildren(path); - if (children.Any()) - { - return DokanResult.DirectoryNotEmpty; - } - - return DokanResult.Success; - } - - public NtStatus MoveFile( - string oldName, - string newName, - bool replace, - IDokanFileInfo info) - { - var success = _storage.MoveNode(oldName, newName, replace); - return success ? DokanResult.Success : DokanResult.FileNotFound; - } - - public NtStatus SetEndOfFile(string fileName, long length, IDokanFileInfo info) - { - var path = FloppyStorage.NormalizePath(fileName); - var node = _storage.GetNode(path); - - if (node == null || node.IsDirectory) - { - return DokanResult.FileNotFound; - } - - var additionalBytes = length - node.Data.Length; - if (additionalBytes > 0 && !_storage.HasEnoughSpace(additionalBytes)) - { - return DokanResult.DiskFull; - } - - if (length != node.Data.Length) - { - var newData = new byte[length]; - var copyLength = Math.Min(length, node.Data.Length); - Array.Copy(node.Data, newData, copyLength); - node.Data = newData; - node.LastWriteTime = DateTime.Now; - } - - return DokanResult.Success; - } - - public NtStatus SetAllocationSize(string fileName, long length, IDokanFileInfo info) - { - var path = FloppyStorage.NormalizePath(fileName); - var node = _storage.GetNode(path); - - if (node == null || node.IsDirectory) - { - return DokanResult.FileNotFound; - } - - if (length < node.Data.Length) - { - var newData = new byte[length]; - Array.Copy(node.Data, newData, length); - node.Data = newData; - node.LastWriteTime = DateTime.Now; - } - - return DokanResult.Success; - } - - public NtStatus LockFile( - string fileName, - long offset, - long length, - IDokanFileInfo info) - { - return DokanResult.Success; - } - - public NtStatus UnlockFile( - string fileName, - long offset, - long length, - IDokanFileInfo info) - { - return DokanResult.Success; - } - - public NtStatus GetDiskFreeSpace( - out long freeBytesAvailable, - out long totalNumberOfBytes, - out long totalNumberOfFreeBytes, - IDokanFileInfo info) - { - freeBytesAvailable = _storage.FreeSpace; - totalNumberOfBytes = _storage.Capacity; - totalNumberOfFreeBytes = _storage.FreeSpace; - return DokanResult.Success; - } - - public NtStatus GetVolumeInformation( - out string volumeLabel, - out FileSystemFeatures features, - out string fileSystemName, - out uint maximumComponentLength, - IDokanFileInfo info) - { - volumeLabel = _volumeName; - fileSystemName = FileSystemName; - maximumComponentLength = 255; - - features = FileSystemFeatures.CasePreservedNames - | FileSystemFeatures.UnicodeOnDisk; - - return DokanResult.Success; - } - - public NtStatus GetFileSecurity( - string fileName, - out FileSystemSecurity? security, - AccessControlSections sections, - IDokanFileInfo info) - { - var path = FloppyStorage.NormalizePath(fileName); - var node = _storage.GetNode(path); - - if (node == null) - { - security = null; - return DokanResult.FileNotFound; - } - - security = node.Security; - return DokanResult.Success; - } - - public NtStatus SetFileSecurity( - string fileName, - FileSystemSecurity security, - AccessControlSections sections, - IDokanFileInfo info) - { - var path = FloppyStorage.NormalizePath(fileName); - var node = _storage.GetNode(path); - - if (node == null) - { - return DokanResult.FileNotFound; - } - - node.Security = security; - return DokanResult.Success; - } - - public NtStatus Mounted(string mountPoint, IDokanFileInfo info) - { - Console.WriteLine($"Drive mounted at {mountPoint}"); - return DokanResult.Success; - } - - public NtStatus Unmounted(IDokanFileInfo info) - { - Console.WriteLine("Drive unmounted"); - return DokanResult.Success; - } - - public NtStatus FindStreams( - string fileName, - out IList streams, - IDokanFileInfo info) - { - streams = []; - return DokanResult.NotImplemented; - } -} diff --git a/FloppyStorage.cs b/FloppyStorage.cs deleted file mode 100644 index 9d1cf0c..0000000 --- a/FloppyStorage.cs +++ /dev/null @@ -1,213 +0,0 @@ -using System.Collections.Concurrent; - -namespace DevDrive; - -/// -/// Manages the in-memory file system storage for the virtual drive. -/// -public class FloppyStorage -{ - public const long DefaultCapacity = 1_474_560; - public const int SectorSize = 512; - public const int AllocationUnitSize = 512; - - private readonly ConcurrentDictionary _files = new(StringComparer.OrdinalIgnoreCase); - private readonly object _lock = new(); - - public long Capacity { get; } - public long FreeSpace { get; } - - public FloppyStorage() : this(DefaultCapacity, DefaultCapacity) - { - } - - public FloppyStorage(long totalBytes, long freeBytes) - { - Capacity = totalBytes; - FreeSpace = freeBytes; - var root = new FloppyFileNode("\\", true); - _files["\\"] = root; - } - - public FloppyFileNode? CreateSymlink(string path, string targetPath, bool isDirectory) - { - path = NormalizePath(path); - - if (_files.ContainsKey(path)) - { - return null; - } - - var parentPath = GetParentPath(path); - if (!_files.ContainsKey(parentPath)) - { - return null; - } - - var node = new FloppyFileNode(path, isDirectory) - { - SymlinkTarget = targetPath, - Attributes = (isDirectory ? FileAttributes.Directory : FileAttributes.Normal) | FileAttributes.ReparsePoint - }; - _files[path] = node; - return node; - } - - public static string NormalizePath(string path) - { - if (string.IsNullOrEmpty(path)) - { - return "\\"; - } - - path = path.Replace('/', '\\'); - if (!path.StartsWith('\\')) - { - path = "\\" + path; - } - - while (path.Contains("\\\\")) - { - path = path.Replace("\\\\", "\\"); - } - - if (path.Length > 1 && path.EndsWith('\\')) - { - path = path.TrimEnd('\\'); - } - - return path; - } - - public static string GetParentPath(string path) - { - path = NormalizePath(path); - if (path == "\\") - { - return "\\"; - } - - var lastSlash = path.LastIndexOf('\\'); - return lastSlash <= 0 ? "\\" : path[..lastSlash]; - } - - public bool Exists(string path) - { - path = NormalizePath(path); - return _files.ContainsKey(path); - } - - public FloppyFileNode? GetNode(string path) - { - path = NormalizePath(path); - _files.TryGetValue(path, out var node); - return node; - } - - public FloppyFileNode? CreateNode(string path, bool isDirectory) - { - path = NormalizePath(path); - - if (_files.ContainsKey(path)) - { - return _files[path]; - } - - var parentPath = GetParentPath(path); - if (!_files.ContainsKey(parentPath)) - { - return null; - } - - var node = new FloppyFileNode(path, isDirectory); - _files[path] = node; - return node; - } - - public bool DeleteNode(string path) - { - path = NormalizePath(path); - - if (path == "\\") - { - return false; - } - - var node = GetNode(path); - if (node == null) - { - return false; - } - - if (node.IsDirectory) - { - var children = GetChildren(path); - if (children.Any()) - { - return false; - } - } - - return _files.TryRemove(path, out _); - } - - public IEnumerable GetChildren(string path) - { - path = NormalizePath(path); - - foreach (var kvp in _files) - { - var filePath = kvp.Key; - if (filePath == path) - { - continue; - } - - var parentPath = GetParentPath(filePath); - if (string.Equals(parentPath, path, StringComparison.OrdinalIgnoreCase)) - { - yield return kvp.Value; - } - } - } - - public bool MoveNode(string oldPath, string newPath, bool replace) - { - oldPath = NormalizePath(oldPath); - newPath = NormalizePath(newPath); - - if (!_files.TryGetValue(oldPath, out var node)) - { - return false; - } - - if (_files.ContainsKey(newPath)) - { - if (!replace) - { - return false; - } - _files.TryRemove(newPath, out _); - } - - var newParentPath = GetParentPath(newPath); - if (!_files.ContainsKey(newParentPath)) - { - return false; - } - - _files.TryRemove(oldPath, out _); - - node.FullPath = newPath; - node.Name = Path.GetFileName(newPath); - node.LastWriteTime = DateTime.Now; - - _files[newPath] = node; - return true; - } - - public bool HasEnoughSpace(long additionalBytes) - { - return FreeSpace >= additionalBytes; - } -} diff --git a/MirrorFileSystem.cs b/MirrorFileSystem.cs new file mode 100644 index 0000000..493007b --- /dev/null +++ b/MirrorFileSystem.cs @@ -0,0 +1,471 @@ +using System.Security.AccessControl; +using DokanNet; +using FileAccess = DokanNet.FileAccess; + +namespace DevDrive; + +/// +/// A Dokan filesystem that mirrors a real folder but reports custom capacity. +/// +public class MirrorFileSystem : IDokanOperations +{ + private const string FileSystemName = "NTFS"; + private const uint VolumeSerial = 0x19950101; + + private readonly string _rootPath; + private string _volumeName; + private long _totalBytes; + private long _freeBytes; + + /// + /// Creates a new mirror filesystem. + /// + /// The real folder to mirror. + /// The volume name to display. + /// Fake total capacity in bytes. + /// Fake free space in bytes. + public MirrorFileSystem(string rootPath, string volumeName, long totalBytes, long freeBytes) + { + _rootPath = rootPath; + _volumeName = volumeName; + _totalBytes = totalBytes; + _freeBytes = freeBytes; + } + + /// + /// Updates the volume metadata (name and capacity) without remounting. + /// + /// New volume name. + /// New total capacity in bytes. + /// New free space in bytes. + public void UpdateMetadata(string volumeName, long totalBytes, long freeBytes) + { + _volumeName = volumeName; + _totalBytes = totalBytes; + _freeBytes = freeBytes; + } + + private string GetRealPath(string fileName) + { + var path = fileName.TrimStart('\\'); + return string.IsNullOrEmpty(path) ? _rootPath : Path.Combine(_rootPath, path); + } + + public NtStatus CreateFile(string fileName, FileAccess access, FileShare share, FileMode mode, + FileOptions options, FileAttributes attributes, IDokanFileInfo info) + { + var realPath = GetRealPath(fileName); + + if (info.IsDirectory) + { + try + { + switch (mode) + { + case FileMode.CreateNew: + if (Directory.Exists(realPath)) + return DokanResult.FileExists; + Directory.CreateDirectory(realPath); + return DokanResult.Success; + + case FileMode.Open: + if (!Directory.Exists(realPath)) + return DokanResult.PathNotFound; + return DokanResult.Success; + + default: + if (!Directory.Exists(realPath)) + return DokanResult.PathNotFound; + return DokanResult.Success; + } + } + catch (UnauthorizedAccessException) + { + return DokanResult.AccessDenied; + } + } + + var pathExists = File.Exists(realPath); + var dirExists = Directory.Exists(realPath); + + if (dirExists) + { + info.IsDirectory = true; + return DokanResult.Success; + } + + try + { + switch (mode) + { + case FileMode.Open: + if (!pathExists) + return DokanResult.FileNotFound; + break; + + case FileMode.CreateNew: + if (pathExists) + return DokanResult.FileExists; + break; + + case FileMode.Truncate: + if (!pathExists) + return DokanResult.FileNotFound; + break; + } + + var fileAccess = System.IO.FileAccess.Read; + if (access.HasFlag(FileAccess.WriteData) || access.HasFlag(FileAccess.AppendData) || + access.HasFlag(FileAccess.Delete) || access.HasFlag(FileAccess.GenericWrite)) + { + fileAccess = System.IO.FileAccess.ReadWrite; + } + + info.Context = new FileStream(realPath, mode, fileAccess, share, 4096, options); + } + catch (UnauthorizedAccessException) + { + return DokanResult.AccessDenied; + } + catch (DirectoryNotFoundException) + { + return DokanResult.PathNotFound; + } + catch (FileNotFoundException) + { + return DokanResult.FileNotFound; + } + catch (IOException) + { + return DokanResult.SharingViolation; + } + + return DokanResult.Success; + } + + public void Cleanup(string fileName, IDokanFileInfo info) + { + (info.Context as FileStream)?.Dispose(); + info.Context = null; + } + + public void CloseFile(string fileName, IDokanFileInfo info) + { + (info.Context as FileStream)?.Dispose(); + info.Context = null; + } + + public NtStatus ReadFile(string fileName, byte[] buffer, out int bytesRead, long offset, IDokanFileInfo info) + { + bytesRead = 0; + + if (info.Context is FileStream stream) + { + lock (stream) + { + stream.Position = offset; + bytesRead = stream.Read(buffer, 0, buffer.Length); + } + return DokanResult.Success; + } + + var realPath = GetRealPath(fileName); + try + { + using var fs = new FileStream(realPath, FileMode.Open, System.IO.FileAccess.Read, FileShare.ReadWrite); + fs.Position = offset; + bytesRead = fs.Read(buffer, 0, buffer.Length); + return DokanResult.Success; + } + catch + { + return DokanResult.Error; + } + } + + public NtStatus WriteFile(string fileName, byte[] buffer, out int bytesWritten, long offset, IDokanFileInfo info) + { + bytesWritten = 0; + + if (info.Context is FileStream stream) + { + lock (stream) + { + stream.Position = offset; + stream.Write(buffer, 0, buffer.Length); + } + bytesWritten = buffer.Length; + return DokanResult.Success; + } + + var realPath = GetRealPath(fileName); + try + { + using var fs = new FileStream(realPath, FileMode.Open, System.IO.FileAccess.Write, FileShare.ReadWrite); + fs.Position = offset; + fs.Write(buffer, 0, buffer.Length); + bytesWritten = buffer.Length; + return DokanResult.Success; + } + catch + { + return DokanResult.Error; + } + } + + public NtStatus FlushFileBuffers(string fileName, IDokanFileInfo info) + { + try + { + (info.Context as FileStream)?.Flush(); + return DokanResult.Success; + } + catch + { + return DokanResult.Error; + } + } + + public NtStatus GetFileInformation(string fileName, out FileInformation fileInfo, IDokanFileInfo info) + { + var realPath = GetRealPath(fileName); + fileInfo = new FileInformation(); + + if (File.Exists(realPath)) + { + var fi = new FileInfo(realPath); + fileInfo.FileName = fi.Name; + fileInfo.Attributes = fi.Attributes; + fileInfo.CreationTime = fi.CreationTime; + fileInfo.LastAccessTime = fi.LastAccessTime; + fileInfo.LastWriteTime = fi.LastWriteTime; + fileInfo.Length = fi.Length; + return DokanResult.Success; + } + + if (Directory.Exists(realPath)) + { + var di = new DirectoryInfo(realPath); + fileInfo.FileName = di.Name; + fileInfo.Attributes = di.Attributes; + fileInfo.CreationTime = di.CreationTime; + fileInfo.LastAccessTime = di.LastAccessTime; + fileInfo.LastWriteTime = di.LastWriteTime; + fileInfo.Length = 0; + return DokanResult.Success; + } + + return DokanResult.FileNotFound; + } + + public NtStatus FindFiles(string fileName, out IList files, IDokanFileInfo info) + { + return FindFilesWithPattern(fileName, "*", out files, info); + } + + public NtStatus FindFilesWithPattern(string fileName, string searchPattern, out IList files, IDokanFileInfo info) + { + files = new List(); + var realPath = GetRealPath(fileName); + + if (!Directory.Exists(realPath)) + return DokanResult.PathNotFound; + + try + { + var dirInfo = new DirectoryInfo(realPath); + foreach (var fsi in dirInfo.EnumerateFileSystemInfos(searchPattern)) + { + files.Add(new FileInformation + { + FileName = fsi.Name, + Attributes = fsi.Attributes, + CreationTime = fsi.CreationTime, + LastAccessTime = fsi.LastAccessTime, + LastWriteTime = fsi.LastWriteTime, + Length = fsi is FileInfo fi ? fi.Length : 0 + }); + } + } + catch + { + return DokanResult.Error; + } + + return DokanResult.Success; + } + + public NtStatus SetFileAttributes(string fileName, FileAttributes attributes, IDokanFileInfo info) + { + try + { + File.SetAttributes(GetRealPath(fileName), attributes); + return DokanResult.Success; + } + catch + { + return DokanResult.Error; + } + } + + public NtStatus SetFileTime(string fileName, DateTime? creationTime, DateTime? lastAccessTime, + DateTime? lastWriteTime, IDokanFileInfo info) + { + var realPath = GetRealPath(fileName); + try + { + if (creationTime.HasValue) + File.SetCreationTime(realPath, creationTime.Value); + if (lastAccessTime.HasValue) + File.SetLastAccessTime(realPath, lastAccessTime.Value); + if (lastWriteTime.HasValue) + File.SetLastWriteTime(realPath, lastWriteTime.Value); + return DokanResult.Success; + } + catch + { + return DokanResult.Error; + } + } + + public NtStatus DeleteFile(string fileName, IDokanFileInfo info) + { + var realPath = GetRealPath(fileName); + if (!File.Exists(realPath)) + return DokanResult.FileNotFound; + return DokanResult.Success; + } + + public NtStatus DeleteDirectory(string fileName, IDokanFileInfo info) + { + var realPath = GetRealPath(fileName); + if (!Directory.Exists(realPath)) + return DokanResult.PathNotFound; + if (Directory.EnumerateFileSystemEntries(realPath).Any()) + return DokanResult.DirectoryNotEmpty; + return DokanResult.Success; + } + + public NtStatus MoveFile(string oldName, string newName, bool replace, IDokanFileInfo info) + { + var oldPath = GetRealPath(oldName); + var newPath = GetRealPath(newName); + + (info.Context as FileStream)?.Dispose(); + info.Context = null; + + try + { + if (info.IsDirectory) + { + if (Directory.Exists(newPath)) + { + if (!replace) + return DokanResult.FileExists; + Directory.Delete(newPath, true); + } + Directory.Move(oldPath, newPath); + } + else + { + if (File.Exists(newPath)) + { + if (!replace) + return DokanResult.FileExists; + File.Delete(newPath); + } + File.Move(oldPath, newPath); + } + return DokanResult.Success; + } + catch + { + return DokanResult.Error; + } + } + + public NtStatus SetEndOfFile(string fileName, long length, IDokanFileInfo info) + { + try + { + if (info.Context is FileStream stream) + { + stream.SetLength(length); + } + else + { + using var fs = new FileStream(GetRealPath(fileName), FileMode.Open, System.IO.FileAccess.Write); + fs.SetLength(length); + } + return DokanResult.Success; + } + catch + { + return DokanResult.Error; + } + } + + public NtStatus SetAllocationSize(string fileName, long length, IDokanFileInfo info) + { + return SetEndOfFile(fileName, length, info); + } + + public NtStatus LockFile(string fileName, long offset, long length, IDokanFileInfo info) + { + return DokanResult.Success; + } + + public NtStatus UnlockFile(string fileName, long offset, long length, IDokanFileInfo info) + { + return DokanResult.Success; + } + + public NtStatus GetDiskFreeSpace(out long freeBytesAvailable, out long totalNumberOfBytes, + out long totalNumberOfFreeBytes, IDokanFileInfo info) + { + freeBytesAvailable = _freeBytes; + totalNumberOfBytes = _totalBytes; + totalNumberOfFreeBytes = _freeBytes; + return DokanResult.Success; + } + + public NtStatus GetVolumeInformation(out string volumeLabel, out FileSystemFeatures features, + out string fileSystemName, out uint maximumComponentLength, IDokanFileInfo info) + { + volumeLabel = _volumeName; + features = FileSystemFeatures.CasePreservedNames | FileSystemFeatures.CaseSensitiveSearch | + FileSystemFeatures.SupportsRemoteStorage | FileSystemFeatures.UnicodeOnDisk; + fileSystemName = FileSystemName; + maximumComponentLength = 256; + return DokanResult.Success; + } + + public NtStatus GetFileSecurity(string fileName, out FileSystemSecurity? security, + AccessControlSections sections, IDokanFileInfo info) + { + security = null; + return DokanResult.NotImplemented; + } + + public NtStatus SetFileSecurity(string fileName, FileSystemSecurity security, + AccessControlSections sections, IDokanFileInfo info) + { + return DokanResult.NotImplemented; + } + + public NtStatus Mounted(string mountPoint, IDokanFileInfo info) + { + return DokanResult.Success; + } + + public NtStatus Unmounted(IDokanFileInfo info) + { + return DokanResult.Success; + } + + public NtStatus FindStreams(string fileName, out IList streams, IDokanFileInfo info) + { + streams = Array.Empty(); + return DokanResult.NotImplemented; + } +} diff --git a/Program.cs b/Program.cs index ee0730c..6cb9a82 100644 --- a/Program.cs +++ b/Program.cs @@ -7,53 +7,66 @@ using DokanNet.Logging; namespace DevDrive; /// -/// Entry point for the virtual drive application. +/// Entry point for the DevDrive application. +/// Uses Dokan to mount a virtual drive with fake capacity and real mklink symlinks. /// [SupportedOSPlatform("windows")] public static class Program { - private const string DefaultMountPoint = "A:\\"; - private const string DefaultConfigDrive = "C:\\PhysicalDrive\\"; + private const string DefaultDriveLetter = "A:"; + private const string DefaultConfigPath = "C:\\PhysicalDrive\\"; private const string ConfigFile = "index.json"; private const string LogPrefix = "[DevDrive]"; + private const string DevDriveFolderName = "DevDriveRoot"; + private static readonly ManualResetEvent ShutdownEvent = new(false); + private static string _driveLetter = DefaultDriveLetter; + private static string _mountPoint = "A:\\"; + private static string _configPath = DefaultConfigPath; + private static string _configFilePath = ""; + private static string _devDrivePath = ""; + private static string _devConfigPath = ""; + private static DevDriveConfig? _lastConfig; + private static readonly object SyncLock = new(); + private static FileSystemWatcher? _devConfigWatcher; private static Dokan? _dokan; private static DokanInstance? _dokanInstance; - private static readonly ManualResetEvent ShutdownEvent = new(false); + private static MirrorFileSystem? _mirrorFs; private static bool _isMounted; - private static string _configDrive = DefaultConfigDrive; - private static string _configFilePath = ""; - private static string _mountPoint = DefaultMountPoint; - private static FloppyConfig? _lastConfig; - private static readonly object MountLock = new(); + private static bool _isUpdatingDevConfig; private static bool _isConfigDriveLetter; /// /// Main entry point. /// - /// Command line arguments. [0] = mount point, [1] = config drive. + /// Command line arguments. [0] = drive letter (e.g. "A:"), [1] = config path. public static void Main(string[] args) { - _mountPoint = args.Length > 0 ? args[0] : DefaultMountPoint; - _configDrive = args.Length > 1 ? args[1] : DefaultConfigDrive; + _driveLetter = args.Length > 0 ? args[0].TrimEnd('\\') : DefaultDriveLetter; + _configPath = args.Length > 1 ? args[1] : DefaultConfigPath; - if (!_mountPoint.EndsWith("\\")) + if (!_driveLetter.EndsWith(":")) { - _mountPoint += "\\"; + _driveLetter += ":"; } - if (!_configDrive.EndsWith("\\")) + _mountPoint = _driveLetter + "\\"; + + if (!_configPath.EndsWith("\\")) { - _configDrive += "\\"; + _configPath += "\\"; } - _configFilePath = Path.Combine(_configDrive, ConfigFile); - _isConfigDriveLetter = _configDrive.Length <= 3 && _configDrive[1] == ':'; + var exeDir = AppContext.BaseDirectory; + _devDrivePath = Path.Combine(exeDir, DevDriveFolderName); + _devConfigPath = Path.Combine(_devDrivePath, "devDriveConfig.json"); + _configFilePath = Path.Combine(_configPath, ConfigFile); + _isConfigDriveLetter = _configPath.Length <= 3 && _configPath[1] == ':'; - Console.WriteLine($"{LogPrefix} DevDrive"); - Console.WriteLine($"{LogPrefix} =================="); - Console.WriteLine($"{LogPrefix} Watching for config drive: {_configDrive}"); + Console.WriteLine($"{LogPrefix} DevDrive (Dokan + mklink mode)"); + Console.WriteLine($"{LogPrefix} =============================="); + Console.WriteLine($"{LogPrefix} Drive letter: {_driveLetter}"); Console.WriteLine($"{LogPrefix} Config file: {_configFilePath}"); - Console.WriteLine($"{LogPrefix} Mount point: {_mountPoint}"); + Console.WriteLine($"{LogPrefix} Root folder: {_devDrivePath}"); Console.WriteLine(); Console.WriteLine($"{LogPrefix} Press Ctrl+C to exit."); Console.WriteLine(); @@ -68,18 +81,20 @@ public static class Program try { _dokan = new Dokan(new ConsoleLogger(LogPrefix)); - + EnsureRootFolder(); + SetupDevConfigWatcher(); + if (_isConfigDriveLetter) { StartWmiWatchers(); } else { - StartFileSystemWatcher(); + StartVolumeWatchers(); } - + CheckAndMountIfReady(); - + ShutdownEvent.WaitOne(); } catch (Exception ex) @@ -89,17 +104,54 @@ public static class Program } finally { - UnmountFloppy(); + CleanupSymlinks(); + UnmountDrive(); + _devConfigWatcher?.Dispose(); _dokan?.Dispose(); } } - private static void StartFileSystemWatcher() + private static void EnsureRootFolder() { - StartVolumeWatchers(); + if (!Directory.Exists(_devDrivePath)) + { + Directory.CreateDirectory(_devDrivePath); + Console.WriteLine($"{LogPrefix} Created root folder: {_devDrivePath}"); + } + } - Console.WriteLine($"{LogPrefix} Volume watchers started for {_configDrive}"); - Console.WriteLine($"{LogPrefix} Watching for: {ConfigFile}"); + private static bool IsConfigAccessible() + { + try + { + return Directory.Exists(_configPath) && File.Exists(_configFilePath); + } + catch + { + return false; + } + } + + private static void StartWmiWatchers() + { + var driveLetter = _configPath.TrimEnd('\\'); + + var insertQuery = new WqlEventQuery( + "SELECT * FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_LogicalDisk'" + ); + var insertWatcher = new ManagementEventWatcher(insertQuery); + insertWatcher.EventArrived += OnDriveInserted; + insertWatcher.Start(); + + var removeQuery = new WqlEventQuery( + "SELECT * FROM __InstanceDeletionEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_LogicalDisk'" + ); + var removeWatcher = new ManagementEventWatcher(removeQuery); + removeWatcher.EventArrived += OnDriveRemoved; + removeWatcher.Start(); + + Console.WriteLine($"{LogPrefix} WMI watchers started (event-based, no polling)"); + Console.WriteLine($"{LogPrefix} Watching for drive: {driveLetter}"); } private static void StartVolumeWatchers() @@ -125,17 +177,19 @@ public static class Program removeWatcher.EventArrived += (s, e) => { Console.WriteLine($"{LogPrefix} Volume removed"); - lock (MountLock) + lock (SyncLock) { if (_isMounted && !IsConfigAccessible()) { Console.WriteLine($"{LogPrefix} Config drive no longer accessible, unmounting..."); - UnmountFloppy(); + UnmountDrive(); _lastConfig = null; } } }; removeWatcher.Start(); + + Console.WriteLine($"{LogPrefix} Volume watchers started for {_configPath}"); } catch (Exception ex) { @@ -143,46 +197,6 @@ public static class Program } } - private static bool IsConfigAccessible() - { - try - { - if (!Directory.Exists(_configDrive)) - { - return false; - } - - var _ = Directory.GetFiles(_configDrive); - return File.Exists(_configFilePath); - } - catch - { - return false; - } - } - - private static void StartWmiWatchers() - { - var driveLetter = _configDrive.TrimEnd('\\'); - - var insertQuery = new WqlEventQuery( - "SELECT * FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_LogicalDisk'" - ); - var insertWatcher = new ManagementEventWatcher(insertQuery); - insertWatcher.EventArrived += OnDriveInserted; - insertWatcher.Start(); - - var removeQuery = new WqlEventQuery( - "SELECT * FROM __InstanceDeletionEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_LogicalDisk'" - ); - var removeWatcher = new ManagementEventWatcher(removeQuery); - removeWatcher.EventArrived += OnDriveRemoved; - removeWatcher.Start(); - - Console.WriteLine($"{LogPrefix} WMI watchers started (event-based, no polling)"); - Console.WriteLine($"{LogPrefix} Watching for drive: {driveLetter}"); - } - private static void OnDriveInserted(object sender, EventArrivedEventArgs e) { try @@ -196,12 +210,12 @@ public static class Program } var insertedDrive = deviceId + "\\"; - - if (string.Equals(insertedDrive, _configDrive, StringComparison.OrdinalIgnoreCase)) + + if (string.Equals(insertedDrive, _configPath, StringComparison.OrdinalIgnoreCase)) { Console.WriteLine(); Console.WriteLine($"{LogPrefix} Drive {deviceId} inserted!"); - + Thread.Sleep(500); CheckAndMountIfReady(); } @@ -225,18 +239,18 @@ public static class Program } var removedDrive = deviceId + "\\"; - - if (string.Equals(removedDrive, _configDrive, StringComparison.OrdinalIgnoreCase)) + + if (string.Equals(removedDrive, _configPath, StringComparison.OrdinalIgnoreCase)) { Console.WriteLine(); Console.WriteLine($"{LogPrefix} Drive {deviceId} removed!"); - - lock (MountLock) + + lock (SyncLock) { if (_isMounted) { Console.WriteLine($"{LogPrefix} Config drive disconnected, unmounting..."); - UnmountFloppy(); + UnmountDrive(); _lastConfig = null; } } @@ -250,14 +264,14 @@ public static class Program private static void CheckAndMountIfReady() { - lock (MountLock) + lock (SyncLock) { - var driveExists = Directory.Exists(_configDrive); + var driveExists = Directory.Exists(_configPath); var configExists = driveExists && File.Exists(_configFilePath); if (!driveExists) { - Console.WriteLine($"{LogPrefix} Waiting for {_configDrive} to be connected..."); + Console.WriteLine($"{LogPrefix} Waiting for {_configPath} to be connected..."); return; } @@ -272,114 +286,55 @@ public static class Program if (config == null || config.Paths.Count == 0) { Console.WriteLine($"{LogPrefix} Config empty or invalid"); - + if (_isMounted) { - UnmountFloppy(); + UnmountDrive(); _lastConfig = null; } return; } - var configChanged = !ConfigEquals(_lastConfig, config); - - if (!_isMounted || configChanged) + if (!_isMounted) { - if (_isMounted && configChanged) - { - Console.WriteLine($"{LogPrefix} Config changed, remounting..."); - UnmountFloppy(); - } - - MountFloppy(config); + EnsureDevConfigFromConfig(config); + MountDrive(); + SyncSymlinksFromConfig(config); _lastConfig = config; } } } - private static FloppyConfig? ReadConfig(string configFilePath) + private static void MountDrive() { - try - { - var json = File.ReadAllText(configFilePath); - return JsonSerializer.Deserialize(json); - } - catch (Exception ex) - { - Console.WriteLine($"{LogPrefix} Error reading config: {ex.Message}"); - return null; - } - } - - private static bool ConfigEquals(FloppyConfig? a, FloppyConfig? b) - { - if (a == null || b == null) - { - return a == b; - } - - if (a.Paths.Count != b.Paths.Count) - { - return false; - } - - for (var i = 0; i < a.Paths.Count; i++) - { - if (a.Paths[i].Name != b.Paths[i].Name || a.Paths[i].Path != b.Paths[i].Path) - { - return false; - } - } - - return true; - } - - private static void MountFloppy(FloppyConfig config) - { - if (_dokan == null) + if (_dokan == null || _isMounted) { return; } + var config = ReadConfig(_configFilePath); + var volumeName = config?.DriveName ?? "Dev Drive"; + var totalBytes = (long)((config?.DriveTotalMB ?? 1.44) * 1024 * 1024); + var freeBytes = (long)((config?.DriveFreeMB ?? 0.72) * 1024 * 1024); + try { - var symlinks = new Dictionary(); - Console.WriteLine(); - Console.WriteLine($"{LogPrefix} Creating symlinks:"); - - foreach (var entry in config.Paths) - { - var symlinkPath = "\\" + entry.Name; - symlinks[symlinkPath] = entry.Path; - Console.WriteLine($"{LogPrefix} {entry.Name} -> {entry.Path}"); - } - - Console.WriteLine(); - - var totalBytes = (long)(config.DriveTotalMB * 1024 * 1024); - var freeBytes = (long)(config.DriveFreeMB * 1024 * 1024); - Console.WriteLine($"{LogPrefix} Drive size: {config.DriveTotalMB} MB total, {config.DriveFreeMB} MB free"); - var floppyFs = new FloppyFileSystem(symlinks, config.DriveName, totalBytes, freeBytes); + _mirrorFs = new MirrorFileSystem(_devDrivePath, volumeName, totalBytes, freeBytes); var dokanBuilder = new DokanInstanceBuilder(_dokan) .ConfigureOptions(options => { - options.Options = DokanOptions.MountManager | DokanOptions.CurrentSession; + options.Options = DokanOptions.FixedDrive | DokanOptions.CurrentSession; options.MountPoint = _mountPoint; options.SingleThread = false; - options.AllocationUnitSize = FloppyStorage.AllocationUnitSize; - options.SectorSize = FloppyStorage.SectorSize; }); - _dokanInstance = dokanBuilder.Build(floppyFs); + _dokanInstance = dokanBuilder.Build(_mirrorFs); _isMounted = true; - Console.WriteLine($"{LogPrefix} Mounted at {_mountPoint}"); - - var iconPath = string.IsNullOrWhiteSpace(config.DriveIcon) - ? "%SystemRoot%\\System32\\shell32.dll,6" - : config.DriveIcon; - DriveIconHelper.SetDriveIcon(_mountPoint, iconPath, config.DriveName); + Console.WriteLine($"{LogPrefix} Mounted {_driveLetter} (Dokan)"); + Console.WriteLine($"{LogPrefix} Volume: {volumeName}"); + Console.WriteLine($"{LogPrefix} Capacity: {config?.DriveTotalMB ?? 1.44} MB total, {config?.DriveFreeMB ?? 0.72} MB free"); } catch (DokanException ex) { @@ -388,7 +343,7 @@ public static class Program } } - private static void UnmountFloppy() + private static void UnmountDrive() { if (!_isMounted) { @@ -397,16 +352,287 @@ public static class Program try { - DriveIconHelper.RemoveDriveIcon(); _dokan?.RemoveMountPoint(_mountPoint); _dokanInstance?.Dispose(); _dokanInstance = null; + _mirrorFs = null; _isMounted = false; - Console.WriteLine($"{LogPrefix} Unmounted"); + Console.WriteLine($"{LogPrefix} Unmounted {_driveLetter}"); } catch (Exception ex) { Console.WriteLine($"{LogPrefix} Unmount error: {ex.Message}"); } } + + private static void SetupDevConfigWatcher() + { + if (_devConfigWatcher != null) + { + return; + } + + if (!Directory.Exists(_devDrivePath)) + { + return; + } + + _devConfigWatcher = new FileSystemWatcher(_devDrivePath) + { + Filter = "devDriveConfig.json", + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime | NotifyFilters.FileName, + EnableRaisingEvents = true + }; + + _devConfigWatcher.Changed += OnDevConfigChanged; + _devConfigWatcher.Created += OnDevConfigChanged; + + Console.WriteLine($"{LogPrefix} Watching devDriveConfig.json for changes..."); + } + + private static void OnDevConfigChanged(object sender, FileSystemEventArgs e) + { + lock (SyncLock) + { + if (_isUpdatingDevConfig) + { + // Change originated from real config sync, ignore. + _isUpdatingDevConfig = false; + return; + } + } + + Thread.Sleep(100); + + try + { + if (!File.Exists(_devConfigPath)) + { + return; + } + + var json = File.ReadAllText(_devConfigPath); + var config = JsonSerializer.Deserialize(json); + if (config == null) + { + Console.WriteLine($"{LogPrefix} devDriveConfig.json change could not be deserialized."); + return; + } + + lock (SyncLock) + { + File.WriteAllText(_configFilePath, json); + + // Check if drive metadata changed (name, capacity) + var metadataChanged = _lastConfig == null || + _lastConfig.DriveName != config.DriveName || + _lastConfig.DriveTotalMB != config.DriveTotalMB || + _lastConfig.DriveFreeMB != config.DriveFreeMB; + + if (metadataChanged && _mirrorFs != null) + { + var volumeName = config.DriveName ?? "Dev Drive"; + var totalBytes = (long)(config.DriveTotalMB * 1024 * 1024); + var freeBytes = (long)(config.DriveFreeMB * 1024 * 1024); + _mirrorFs.UpdateMetadata(volumeName, totalBytes, freeBytes); + Console.WriteLine($"{LogPrefix} Updated drive metadata: {volumeName}, {config.DriveTotalMB} MB total, {config.DriveFreeMB} MB free"); + } + + SyncSymlinksFromConfig(config); + _lastConfig = config; + } + } + catch (Exception ex) + { + Console.WriteLine($"{LogPrefix} Error processing devDriveConfig.json change: {ex.Message}"); + } + } + + private static DevDriveConfig? ReadConfig(string path) + { + try + { + var json = File.ReadAllText(path); + return JsonSerializer.Deserialize(json); + } + catch (Exception ex) + { + Console.WriteLine($"{LogPrefix} Error reading config: {ex.Message}"); + return null; + } + } + + private static void SyncSymlinksFromConfig(DevDriveConfig config) + { + Console.WriteLine(); + Console.WriteLine($"{LogPrefix} Syncing symlinks (mklink /d):"); + + var existingEntries = Directory.GetFileSystemEntries(_devDrivePath) + .Where(p => !Path.GetFileName(p).Equals("desktop.ini", StringComparison.OrdinalIgnoreCase)) + .Where(p => !Path.GetFileName(p).Equals("devDriveConfig.json", StringComparison.OrdinalIgnoreCase)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var configPaths = config.Paths + .Select(p => Path.Combine(_devDrivePath, p.Name)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var entry in existingEntries) + { + if (!configPaths.Contains(entry)) + { + RemoveSymlink(entry); + } + } + + foreach (var pathEntry in config.Paths) + { + var symlinkPath = Path.Combine(_devDrivePath, pathEntry.Name); + CreateSymlink(symlinkPath, pathEntry.Path); + } + + Console.WriteLine(); + } + + private static void EnsureDevConfigFromConfig(DevDriveConfig config) + { + try + { + _isUpdatingDevConfig = true; + var json = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(_devConfigPath, json); + Console.WriteLine($"{LogPrefix} Created devDriveConfig.json from index.json"); + } + catch (Exception ex) + { + Console.WriteLine($"{LogPrefix} Error creating devDriveConfig.json: {ex.Message}"); + } + } + + private static void CreateSymlink(string symlinkPath, string targetPath) + { + try + { + if (Directory.Exists(symlinkPath) || File.Exists(symlinkPath)) + { + var attr = File.GetAttributes(symlinkPath); + if (attr.HasFlag(FileAttributes.ReparsePoint)) + { + var existingTarget = new FileInfo(symlinkPath).LinkTarget; + if (existingTarget?.Equals(targetPath, StringComparison.OrdinalIgnoreCase) == true) + { + Console.WriteLine($"{LogPrefix} {Path.GetFileName(symlinkPath)} -> {targetPath} (unchanged)"); + return; + } + RemoveSymlink(symlinkPath); + } + else + { + Console.WriteLine($"{LogPrefix} Skipping {Path.GetFileName(symlinkPath)} - not a symlink"); + return; + } + } + + var isDirectory = Directory.Exists(targetPath); + var args = isDirectory + ? $"/c mklink /d \"{symlinkPath}\" \"{targetPath}\"" + : $"/c mklink \"{symlinkPath}\" \"{targetPath}\""; + + var result = RunCommand("cmd.exe", args); + + if (result.ExitCode == 0) + { + Console.WriteLine($"{LogPrefix} {Path.GetFileName(symlinkPath)} -> {targetPath}"); + } + else + { + Console.WriteLine($"{LogPrefix} Failed: {Path.GetFileName(symlinkPath)} - {result.Error}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"{LogPrefix} Error: {Path.GetFileName(symlinkPath)} - {ex.Message}"); + } + } + + private static void RemoveSymlink(string path) + { + try + { + var attr = File.GetAttributes(path); + if (attr.HasFlag(FileAttributes.Directory)) + { + Directory.Delete(path, false); + } + else + { + File.Delete(path); + } + Console.WriteLine($"{LogPrefix} Removed: {Path.GetFileName(path)}"); + } + catch (Exception ex) + { + Console.WriteLine($"{LogPrefix} Error removing {Path.GetFileName(path)}: {ex.Message}"); + } + } + + private static void CleanupSymlinks() + { + if (!Directory.Exists(_devDrivePath)) + { + return; + } + + Console.WriteLine($"{LogPrefix} Cleaning up symlinks..."); + + var entries = Directory.GetFileSystemEntries(_devDrivePath) + .Where(p => !Path.GetFileName(p).Equals("desktop.ini", StringComparison.OrdinalIgnoreCase)) + .Where(p => !Path.GetFileName(p).Equals("devDriveConfig.json", StringComparison.OrdinalIgnoreCase)); + + foreach (var entry in entries) + { + try + { + var attr = File.GetAttributes(entry); + if (attr.HasFlag(FileAttributes.ReparsePoint)) + { + if (attr.HasFlag(FileAttributes.Directory)) + { + Directory.Delete(entry, false); + } + else + { + File.Delete(entry); + } + + Console.WriteLine($"{LogPrefix} Removed: {Path.GetFileName(entry)}"); + } + } + catch + { + } + } + } + + private static (int ExitCode, string Output, string Error) RunCommand(string fileName, string arguments) + { + var process = new System.Diagnostics.Process + { + StartInfo = new System.Diagnostics.ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + + process.Start(); + var output = process.StandardOutput.ReadToEnd(); + var error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + + return (process.ExitCode, output.Trim(), error.Trim()); + } } diff --git a/SymlinkHandler.cs b/SymlinkHandler.cs deleted file mode 100644 index 0b21381..0000000 --- a/SymlinkHandler.cs +++ /dev/null @@ -1,201 +0,0 @@ -using DokanNet; - -namespace DevDrive; - -/// -/// Handles passthrough operations for symlinks to real file system paths. -/// -public static class SymlinkHandler -{ - public static bool TryResolveSymlink(string virtualPath, FloppyStorage storage, out string? realPath) - { - realPath = null; - virtualPath = FloppyStorage.NormalizePath(virtualPath); - - var node = storage.GetNode(virtualPath); - if (node?.IsSymlink == true) - { - realPath = node.SymlinkTarget; - return true; - } - - var parts = virtualPath.Split('\\', StringSplitOptions.RemoveEmptyEntries); - var currentPath = "\\"; - - for (var i = 0; i < parts.Length; i++) - { - currentPath = currentPath == "\\" ? "\\" + parts[i] : currentPath + "\\" + parts[i]; - var currentNode = storage.GetNode(currentPath); - - if (currentNode?.IsSymlink == true) - { - var remainingPath = string.Join("\\", parts.Skip(i + 1)); - realPath = string.IsNullOrEmpty(remainingPath) - ? currentNode.SymlinkTarget - : Path.Combine(currentNode.SymlinkTarget!, remainingPath); - return true; - } - } - - return false; - } - - public static IList ListRealDirectory(string realPath) - { - var files = new List(); - - try - { - if (!Directory.Exists(realPath)) - { - return files; - } - - foreach (var entry in new DirectoryInfo(realPath).EnumerateFileSystemInfos()) - { - files.Add(new FileInformation - { - FileName = entry.Name, - Attributes = entry.Attributes, - CreationTime = entry.CreationTime, - LastAccessTime = entry.LastAccessTime, - LastWriteTime = entry.LastWriteTime, - Length = entry is FileInfo fi ? fi.Length : 0 - }); - } - } - catch - { - } - - return files; - } - - public static bool GetRealFileInfo(string realPath, out FileInformation fileInfo) - { - fileInfo = default; - - try - { - if (File.Exists(realPath)) - { - var fi = new FileInfo(realPath); - fileInfo = new FileInformation - { - FileName = fi.Name, - Attributes = fi.Attributes, - CreationTime = fi.CreationTime, - LastAccessTime = fi.LastAccessTime, - LastWriteTime = fi.LastWriteTime, - Length = fi.Length - }; - return true; - } - - if (Directory.Exists(realPath)) - { - var di = new DirectoryInfo(realPath); - fileInfo = new FileInformation - { - FileName = di.Name, - Attributes = di.Attributes, - CreationTime = di.CreationTime, - LastAccessTime = di.LastAccessTime, - LastWriteTime = di.LastWriteTime, - Length = 0 - }; - return true; - } - } - catch - { - } - - return false; - } - - public static NtStatus ReadRealFile(string realPath, byte[] buffer, out int bytesRead, long offset) - { - bytesRead = 0; - - try - { - using var stream = new FileStream(realPath, System.IO.FileMode.Open, System.IO.FileAccess.Read, FileShare.ReadWrite); - stream.Seek(offset, SeekOrigin.Begin); - bytesRead = stream.Read(buffer, 0, buffer.Length); - return DokanResult.Success; - } - catch (FileNotFoundException) - { - return DokanResult.FileNotFound; - } - catch (UnauthorizedAccessException) - { - return DokanResult.AccessDenied; - } - catch - { - return DokanResult.Error; - } - } - - public static NtStatus WriteRealFile(string realPath, byte[] buffer, out int bytesWritten, long offset) - { - bytesWritten = 0; - - try - { - using var stream = new FileStream(realPath, System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.Write, FileShare.ReadWrite); - stream.Seek(offset, SeekOrigin.Begin); - stream.Write(buffer, 0, buffer.Length); - bytesWritten = buffer.Length; - return DokanResult.Success; - } - catch (UnauthorizedAccessException) - { - return DokanResult.AccessDenied; - } - catch - { - return DokanResult.Error; - } - } - - public static NtStatus CreateRealFileOrDirectory(string realPath, bool isDirectory, FileMode mode) - { - try - { - if (isDirectory) - { - if (mode == FileMode.CreateNew) - { - Directory.CreateDirectory(realPath); - } - return DokanResult.Success; - } - - switch (mode) - { - case FileMode.CreateNew: - using (File.Create(realPath)) { } - break; - case FileMode.Create: - case FileMode.OpenOrCreate: - using (new FileStream(realPath, System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.ReadWrite)) { } - break; - default: - break; - } - - return DokanResult.Success; - } - catch (UnauthorizedAccessException) - { - return DokanResult.AccessDenied; - } - catch - { - return DokanResult.Error; - } - } -}