472 lines
14 KiB
C#
472 lines
14 KiB
C#
using System.Security.AccessControl;
|
|
using DokanNet;
|
|
using FileAccess = DokanNet.FileAccess;
|
|
|
|
namespace DevDrive;
|
|
|
|
/// <summary>
|
|
/// A Dokan filesystem that mirrors a real folder but reports custom capacity.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Creates a new mirror filesystem.
|
|
/// </summary>
|
|
/// <param name="rootPath">The real folder to mirror.</param>
|
|
/// <param name="volumeName">The volume name to display.</param>
|
|
/// <param name="totalBytes">Fake total capacity in bytes.</param>
|
|
/// <param name="freeBytes">Fake free space in bytes.</param>
|
|
public MirrorFileSystem(string rootPath, string volumeName, long totalBytes, long freeBytes)
|
|
{
|
|
_rootPath = rootPath;
|
|
_volumeName = volumeName;
|
|
_totalBytes = totalBytes;
|
|
_freeBytes = freeBytes;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the volume metadata (name and capacity) without remounting.
|
|
/// </summary>
|
|
/// <param name="volumeName">New volume name.</param>
|
|
/// <param name="totalBytes">New total capacity in bytes.</param>
|
|
/// <param name="freeBytes">New free space in bytes.</param>
|
|
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<FileInformation> files, IDokanFileInfo info)
|
|
{
|
|
return FindFilesWithPattern(fileName, "*", out files, info);
|
|
}
|
|
|
|
public NtStatus FindFilesWithPattern(string fileName, string searchPattern, out IList<FileInformation> files, IDokanFileInfo info)
|
|
{
|
|
files = new List<FileInformation>();
|
|
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<FileInformation> streams, IDokanFileInfo info)
|
|
{
|
|
streams = Array.Empty<FileInformation>();
|
|
return DokanResult.NotImplemented;
|
|
}
|
|
}
|