Add Explorer Workbench with hierarchical, off-UI Storage analysis.

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>
This commit is contained in:
2026-08-22 12:43:05 +02:00
commit e9aba73552
130 changed files with 15110 additions and 0 deletions

View File

@@ -0,0 +1,209 @@
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;
}
}