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 _logger; public WindowsShellFileOperations(ILogger 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 paths, out string? error) => Delete(paths, recycle: true, out error); public bool Delete(IReadOnlyList paths, bool recycle, out string? error) { error = null; if (paths.Count == 0) { return true; } var packed = string.Join("\0", paths.Select(PathRules.FromExtended)) + "\0\0"; var pFrom = Marshal.StringToHGlobalUni(packed); try { var flags = NativeMethods.FofNoConfirmation | NativeMethods.FofNoErrorUi; if (recycle) { flags |= NativeMethods.FofAllowUndo; } var op = new NativeMethods.ShFileOpStruct { hwnd = NativeMethods.GetForegroundWindow(), wFunc = (uint)NativeMethods.FoDelete, pFrom = pFrom, pTo = 0, fFlags = (ushort)flags, fAnyOperationsAborted = 0, hNameMappings = 0, lpszProgressTitle = null }; var rc = NativeMethods.SHFileOperation(ref op); if (op.fAnyOperationsAborted != 0) { error = "Cancelled"; return false; } if (rc != 0) { error = recycle ? $"Recycle failed ({rc})" : $"Delete failed ({rc})"; _logger.LogWarning("SHFileOperation delete returned {Code} recycle={Recycle}", rc, recycle); return false; } return true; } finally { Marshal.FreeHGlobal(pFrom); } } public bool CopyFileWithProgress(string source, string destination, bool overwrite, IProgress? 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? 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 bool CreateShortcut(string targetPath, string shortcutPath, out string? error) { error = null; try { var type = Type.GetTypeFromProgID("WScript.Shell"); if (type is null) { error = "Shortcut service is unavailable."; return false; } dynamic shell = Activator.CreateInstance(type)!; dynamic shortcut = shell.CreateShortcut(PathRules.FromExtended(shortcutPath)); var target = PathRules.FromExtended(targetPath); shortcut.TargetPath = target; shortcut.WorkingDirectory = Directory.Exists(target) ? target : PathRules.Parent(target); shortcut.Save(); return true; } catch (Exception ex) { error = ex.Message; _logger.LogDebug(ex, "CreateShortcut failed for {Target}", targetPath); return false; } } } 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); } }