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