Storage queries run in the background with cancellation and covering indexes so switching views no longer freezes the UI. Co-authored-by: Cursor <cursoragent@cursor.com>
210 lines
7.1 KiB
C#
210 lines
7.1 KiB
C#
using System.ComponentModel;
|
|
using System.Runtime.InteropServices;
|
|
using Explorer.Domain.Abstractions;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Explorer.Windows;
|
|
|
|
public sealed class WindowsUsnJournal : IUsnJournal
|
|
{
|
|
private readonly ILogger<WindowsUsnJournal> _logger;
|
|
|
|
public WindowsUsnJournal(ILogger<WindowsUsnJournal> logger) => _logger = logger;
|
|
|
|
public bool TryQuery(string rootPath, out UsnJournalState state, out string? error)
|
|
{
|
|
state = new UsnJournalState();
|
|
error = null;
|
|
var volume = OpenVolume(rootPath, out error);
|
|
if (volume == nint.Zero)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
var size = Marshal.SizeOf<NativeMethods.UsnJournalDataV2>();
|
|
var buffer = Marshal.AllocHGlobal(size);
|
|
try
|
|
{
|
|
if (!NativeMethods.DeviceIoControl(volume, NativeMethods.IoctlQueryUsnJournal, 0, 0, buffer, (uint)size, out _, 0))
|
|
{
|
|
error = new Win32Exception(Marshal.GetLastWin32Error()).Message;
|
|
return false;
|
|
}
|
|
|
|
var data = Marshal.PtrToStructure<NativeMethods.UsnJournalDataV2>(buffer);
|
|
state = new UsnJournalState
|
|
{
|
|
JournalId = unchecked((long)data.UsnJournalId),
|
|
NextUsn = data.NextUsn
|
|
};
|
|
return true;
|
|
}
|
|
finally
|
|
{
|
|
Marshal.FreeHGlobal(buffer);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = ex.Message;
|
|
_logger.LogDebug(ex, "USN query failed for {Path}", rootPath);
|
|
return false;
|
|
}
|
|
finally
|
|
{
|
|
NativeMethods.CloseHandle(volume);
|
|
}
|
|
}
|
|
|
|
public IReadOnlyList<UsnRecord> Read(string rootPath, UsnJournalState from, int maxRecords, out UsnJournalState next, out UsnReadStatus status)
|
|
{
|
|
next = from;
|
|
status = UsnReadStatus.Unavailable;
|
|
var records = new List<UsnRecord>();
|
|
if (!TryQuery(rootPath, out var current, out var error))
|
|
{
|
|
status = error?.Contains("denied", StringComparison.OrdinalIgnoreCase) == true
|
|
? UsnReadStatus.AccessDenied
|
|
: UsnReadStatus.Unavailable;
|
|
return records;
|
|
}
|
|
|
|
if (from.JournalId != 0 && from.JournalId != current.JournalId)
|
|
{
|
|
status = UsnReadStatus.JournalReset;
|
|
next = current;
|
|
return records;
|
|
}
|
|
|
|
var volume = OpenVolume(rootPath, out _);
|
|
if (volume == nint.Zero)
|
|
{
|
|
status = UsnReadStatus.AccessDenied;
|
|
return records;
|
|
}
|
|
|
|
try
|
|
{
|
|
var read = new NativeMethods.ReadUsnJournalDataV1
|
|
{
|
|
StartUsn = from.NextUsn == 0 ? current.NextUsn : from.NextUsn,
|
|
ReasonMask = 0xFFFFFFFF,
|
|
ReturnOnlyOnClose = 0,
|
|
Timeout = 0,
|
|
BytesToWaitFor = 0,
|
|
UsnJournalId = unchecked((ulong)current.JournalId),
|
|
MinMajorVersion = 2,
|
|
MaxMajorVersion = 3
|
|
};
|
|
|
|
var inSize = Marshal.SizeOf(read);
|
|
var inPtr = Marshal.AllocHGlobal(inSize);
|
|
var outSize = 64 * 1024;
|
|
var outPtr = Marshal.AllocHGlobal(outSize);
|
|
try
|
|
{
|
|
Marshal.StructureToPtr(read, inPtr, false);
|
|
if (!NativeMethods.DeviceIoControl(volume, NativeMethods.IoctlReadUsnJournal, inPtr, (uint)inSize, outPtr, (uint)outSize, out var returned, 0))
|
|
{
|
|
var code = Marshal.GetLastWin32Error();
|
|
status = code is 5 or 1314 ? UsnReadStatus.AccessDenied : UsnReadStatus.Error;
|
|
if (code is 1179 or 1180)
|
|
{
|
|
status = UsnReadStatus.JournalReset;
|
|
}
|
|
|
|
return records;
|
|
}
|
|
|
|
if (returned < 8)
|
|
{
|
|
status = UsnReadStatus.Ok;
|
|
next = current;
|
|
return records;
|
|
}
|
|
|
|
var nextUsn = Marshal.ReadInt64(outPtr);
|
|
var offset = 8;
|
|
while (offset + 60 < returned && records.Count < maxRecords)
|
|
{
|
|
var recordLength = Marshal.ReadInt32(outPtr, offset);
|
|
if (recordLength <= 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
var major = Marshal.ReadInt16(outPtr, offset + 4);
|
|
var frn = Marshal.ReadInt64(outPtr, offset + 8);
|
|
var parentFrn = Marshal.ReadInt64(outPtr, offset + 16);
|
|
var usn = Marshal.ReadInt64(outPtr, offset + 24);
|
|
var reason = Marshal.ReadInt32(outPtr, offset + 40);
|
|
var attrs = Marshal.ReadInt32(outPtr, offset + 52);
|
|
var nameLength = Marshal.ReadInt16(outPtr, offset + 56);
|
|
var nameOffset = Marshal.ReadInt16(outPtr, offset + 58);
|
|
var name = Marshal.PtrToStringUni(outPtr + offset + nameOffset, nameLength / 2) ?? "";
|
|
records.Add(new UsnRecord
|
|
{
|
|
FileReferenceNumber = frn,
|
|
ParentFileReferenceNumber = parentFrn,
|
|
Usn = usn,
|
|
FileName = name,
|
|
Reason = reason,
|
|
FileAttributes = attrs
|
|
});
|
|
offset += recordLength;
|
|
_ = major;
|
|
}
|
|
|
|
next = new UsnJournalState { JournalId = current.JournalId, NextUsn = nextUsn };
|
|
status = UsnReadStatus.Ok;
|
|
return records;
|
|
}
|
|
finally
|
|
{
|
|
Marshal.FreeHGlobal(inPtr);
|
|
Marshal.FreeHGlobal(outPtr);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogDebug(ex, "USN read failed for {Path}", rootPath);
|
|
status = UsnReadStatus.Error;
|
|
return records;
|
|
}
|
|
finally
|
|
{
|
|
NativeMethods.CloseHandle(volume);
|
|
}
|
|
}
|
|
|
|
private static nint OpenVolume(string rootPath, out string? error)
|
|
{
|
|
error = null;
|
|
var letter = Path.GetPathRoot(rootPath)?.TrimEnd('\\');
|
|
if (string.IsNullOrEmpty(letter))
|
|
{
|
|
error = "Invalid volume";
|
|
return nint.Zero;
|
|
}
|
|
|
|
var volumePath = @"\\.\" + letter;
|
|
var handle = NativeMethods.CreateFile(
|
|
volumePath,
|
|
NativeMethods.GenericRead,
|
|
NativeMethods.FileShareRead | NativeMethods.FileShareWrite,
|
|
0,
|
|
NativeMethods.OpenExisting,
|
|
0,
|
|
0);
|
|
if (handle == nint.Zero || handle == new nint(-1))
|
|
{
|
|
error = new Win32Exception(Marshal.GetLastWin32Error()).Message;
|
|
return nint.Zero;
|
|
}
|
|
|
|
return handle;
|
|
}
|
|
}
|