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; } }