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:
144
src/Explorer.Windows/WindowsShellFileOperations.cs
Normal file
144
src/Explorer.Windows/WindowsShellFileOperations.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Explorer.Domain;
|
||||
using Explorer.Domain.Abstractions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Explorer.Windows;
|
||||
|
||||
public sealed class WindowsShellFileOperations : IShellFileOperations
|
||||
{
|
||||
private readonly ILogger<WindowsShellFileOperations> _logger;
|
||||
|
||||
public WindowsShellFileOperations(ILogger<WindowsShellFileOperations> logger) => _logger = logger;
|
||||
|
||||
public void Open(string path)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = PathRules.FromExtended(path),
|
||||
UseShellExecute = true
|
||||
};
|
||||
Process.Start(psi);
|
||||
}
|
||||
|
||||
public bool DeleteToRecycleBin(IReadOnlyList<string> paths, out string? error)
|
||||
{
|
||||
error = null;
|
||||
if (paths.Count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var joined = string.Join("\0", paths.Select(PathRules.FromExtended)) + "\0\0";
|
||||
var op = new NativeMethods.ShFileOpStruct
|
||||
{
|
||||
hwnd = 0,
|
||||
wFunc = NativeMethods.FoDelete,
|
||||
pFrom = joined,
|
||||
pTo = null!,
|
||||
fFlags = (ushort)(NativeMethods.FofAllowUndo | NativeMethods.FofNoConfirmation | NativeMethods.FofNoErrorUi | NativeMethods.FofSilent),
|
||||
fAnyOperationsAborted = 0,
|
||||
hNameMappings = 0,
|
||||
lpszProgressTitle = null
|
||||
};
|
||||
|
||||
var rc = NativeMethods.SHFileOperation(ref op);
|
||||
if (rc != 0)
|
||||
{
|
||||
error = $"Recycle failed ({rc})";
|
||||
_logger.LogWarning("SHFileOperation delete returned {Code}", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool CopyFileWithProgress(string source, string destination, bool overwrite, IProgress<long>? progress, CancellationToken cancellationToken, out string? error)
|
||||
{
|
||||
error = null;
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(PathRules.FromExtended(destination))!);
|
||||
var cancel = 0;
|
||||
NativeMethods.CopyProgressRoutine cb = (total, transferred, _, _, _, _, _, _, _) =>
|
||||
{
|
||||
progress?.Report(transferred);
|
||||
return cancellationToken.IsCancellationRequested ? NativeMethods.ProgressCancel : NativeMethods.ProgressContinue;
|
||||
};
|
||||
|
||||
var flags = overwrite ? 0u : NativeMethods.CopyFileFailIfExists;
|
||||
var ok = NativeMethods.CopyFileEx(
|
||||
PathRules.ToExtended(source),
|
||||
PathRules.ToExtended(destination),
|
||||
cb,
|
||||
0,
|
||||
ref cancel,
|
||||
flags);
|
||||
if (!ok)
|
||||
{
|
||||
var code = Marshal.GetLastWin32Error();
|
||||
if (cancellationToken.IsCancellationRequested || code == 1235)
|
||||
{
|
||||
error = "Cancelled";
|
||||
return false;
|
||||
}
|
||||
|
||||
error = new System.ComponentModel.Win32Exception(code).Message;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool MoveFileWithProgress(string source, string destination, bool overwrite, IProgress<long>? progress, CancellationToken cancellationToken, out string? error)
|
||||
{
|
||||
error = null;
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(PathRules.FromExtended(destination))!);
|
||||
NativeMethods.CopyProgressRoutine cb = (total, transferred, _, _, _, _, _, _, _) =>
|
||||
{
|
||||
progress?.Report(transferred);
|
||||
return cancellationToken.IsCancellationRequested ? NativeMethods.ProgressCancel : NativeMethods.ProgressContinue;
|
||||
};
|
||||
|
||||
var flags = NativeMethods.MoveFileCopyAllowed | NativeMethods.MoveFileWriteThrough;
|
||||
if (overwrite)
|
||||
{
|
||||
flags |= NativeMethods.MoveFileReplaceExisting;
|
||||
}
|
||||
|
||||
var ok = NativeMethods.MoveFileWithProgress(
|
||||
PathRules.ToExtended(source),
|
||||
PathRules.ToExtended(destination),
|
||||
cb,
|
||||
0,
|
||||
flags);
|
||||
if (!ok)
|
||||
{
|
||||
var code = Marshal.GetLastWin32Error();
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
error = "Cancelled";
|
||||
return false;
|
||||
}
|
||||
|
||||
error = new System.ComponentModel.Win32Exception(code).Message;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class BackgroundIo
|
||||
{
|
||||
public static IDisposable Begin()
|
||||
{
|
||||
NativeMethods.SetPriorityClass(NativeMethods.GetCurrentProcess(), NativeMethods.ProcessModeBackgroundBegin);
|
||||
return new Reset();
|
||||
}
|
||||
|
||||
private sealed class Reset : IDisposable
|
||||
{
|
||||
public void Dispose()
|
||||
=> NativeMethods.SetPriorityClass(NativeMethods.GetCurrentProcess(), NativeMethods.ProcessModeBackgroundEnd);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user