196 lines
6.2 KiB
C#
196 lines
6.2 KiB
C#
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)
|
|
=> Delete(paths, recycle: true, out error);
|
|
|
|
public bool Delete(IReadOnlyList<string> 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<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 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);
|
|
}
|
|
}
|