Vanara/BITS/BackgroundCopyManager.cs

227 lines
8.2 KiB
C#

using System.Diagnostics.CodeAnalysis;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using Vanara.PInvoke;
using static Vanara.PInvoke.BITS;
namespace Vanara.IO;
/// <summary>
/// Use the BackgroundCopyManager to create transfer jobs, retrieve an enumerator object that contains the jobs in the queue, and to
/// retrieve individual jobs from the queue.
/// </summary>
public static partial class BackgroundCopyManager
{
private static readonly ComReleaser<IBackgroundCopyManager> ciMgr = ComReleaserFactory.Create(new IBackgroundCopyManager());
[MaybeNull]
private static Version ver;
/// <summary>Gets the list of currently queued jobs for all users.</summary>
public static BackgroundCopyJobCollection Jobs { get; } = new BackgroundCopyJobCollection();
/// <summary>Gets an object that manages the pool of peers from which you can download content.</summary>
/// <value>The peer cache administration.</value>
public static PeerCacheAdministration PeerCacheAdministration { get; } = new PeerCacheAdministration(ciMgr.Item);
/// <summary>Retrieves the running version of BITS.</summary>
public static Version Version
{
get
{
try { return ver ??= GetVer(); }
catch { return new Version(); }
static Version GetVer()
{
string dllPath = Microsoft.Win32.Registry.GetValue($@"HKEY_CLASSES_ROOT\CLSID\{typeof(IBackgroundCopyManager).GUID:B}\InProcServer32", null, null)?.ToString() ??
Environment.ExpandEnvironmentVariables($@"%SystemRoot%\{(Environment.Is64BitProcess != Environment.Is64BitOperatingSystem ? "sysnative" : "system32")}\qmgr.dll");
var fi = System.Diagnostics.FileVersionInfo.GetVersionInfo(dllPath);
return $"{fi.FileMajorPart}.{fi.FileMinorPart}" switch
{
"7.8" when fi.FileBuildPart >= 18362 => new Version(10, 3),
"7.8" when fi.FileBuildPart >= 17763 => new Version(10, 2),
"7.8" => new Version(10, 1),
"7.7" => new Version(5, 0),
"7.5" => new Version(4, 0),
"7.0" => new Version(3, 0),
"6.7" => new Version(2, 5),
"6.6" => new Version(2, 0),
"6.5" => new Version(1, 5),
"6.2" => new Version(1, 2),
_ => new Version(1, 0),
};
}
}
}
internal static IBackgroundCopyManager IMgr => ciMgr.Item;
/// <summary>Copies an existing file to a new file using BITS. Overwriting a file of the same name is not allowed.</summary>
/// <param name="sourceFileName">The file to copy.</param>
/// <param name="destFileName">The name of the destination file.</param>
public static void Copy(string sourceFileName, string destFileName) => CopyTemplate(destFileName, new CancellationToken(false), null, f => f.Add(sourceFileName, destFileName));
/// <summary>Copies an existing file to a new file using BITS. Overwriting a file of the same name is not allowed.</summary>
/// <param name="sourceFileName">The file to copy.</param>
/// <param name="destFileName">The name of the destination file.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <param name="progress">An optional delegate that can be used to track the progress of the operation asynchronously.</param>
/// <returns>A task that represents the asynchronous copy operation.</returns>
/// <example>
/// Below is an example of how to use this function with cancellation support and a progress delegate:
/// <code title="Async background file copy">
/// // Using a cancellation token allows the UI to respond to a cancellation request
/// var cts = new CancellationTokenSource();
/// // This progress delegate writes the status to the console, but it could also up the UI
/// var prog = new Progress&lt;Tuple&lt;BackgroundCopyJobState, byte&gt;&gt;(t =&gt; Console.WriteLine($"{t.Item2}% : {t.Item1}"));
/// // This optionally tells the copy routine to cancel if it takes longer than 2 seconds.
/// // You don't need this and can rely on other user cancellation methods.
/// cts.CancelAfter(2000);
/// // This is the copy routine.
/// // 'src' is the source file path.
/// // 'dest' is the destination file path.
/// BackgroundCopyManager.CopyAsync(src, dest, cts.Token, prog);
/// </code>
/// </example>
public static async Task CopyAsync(string sourceFileName, string destFileName, [Optional] CancellationToken cancellationToken, [Optional] IProgress<Tuple<BackgroundCopyJobState, byte>>? progress)
{
#if NET40
await TaskEx.Run(() => CopyTemplate(destFileName, cancellationToken, (s, p) => progress?.Report(new Tuple<BackgroundCopyJobState, byte>(s,p)), f => f.Add(sourceFileName, destFileName)), cancellationToken);
#else
await Task.Run(() => CopyTemplate(destFileName, cancellationToken, (s, p) => progress?.Report(new Tuple<BackgroundCopyJobState, byte>(s, p)), f => f.Add(sourceFileName, destFileName)), cancellationToken);
#endif
cancellationToken.ThrowIfCancellationRequested();
}
internal static IBackgroundCopyJob CreateJob(string displayName, BG_JOB_TYPE jobType = BG_JOB_TYPE.BG_JOB_TYPE_DOWNLOAD)
{
try
{
IMgr.CreateJob(displayName, jobType, out Guid newJobID, out IBackgroundCopyJob newJob);
return newJob;
}
catch (COMException cex)
{
throw new BackgroundCopyException(cex);
}
}
internal static IEnumBackgroundCopyJobs EnumJobs(BG_JOB_ENUM type = BG_JOB_ENUM.BG_JOB_ENUM_ALL_USERS) => IMgr.EnumJobs(type);
internal static string? GetErrorMessage(HRESULT hResult)
{
try
{
return IMgr.GetErrorDescription(hResult, (uint)((short)System.Globalization.CultureInfo.CurrentCulture.LCID));
}
catch (COMException)
{
return null;
}
}
internal static IBackgroundCopyJob? GetJob(Guid jobId)
{
try
{
return IMgr.GetJob(jobId);
}
catch (COMException cex)
{
if ((uint)cex.ErrorCode != 0x80200001)
throw new BackgroundCopyException(cex);
}
return null;
}
/// <summary>Checks if the current user has administrator rights.</summary>
internal static bool IsCurrentUserAdministrator()
{
using var identity = WindowsIdentity.GetCurrent();
return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator);
}
private static void CopyTemplate(string destFileName, CancellationToken ct, Action<BackgroundCopyJobState, byte>? report, Action<BackgroundCopyFileCollection> add)
{
BackgroundCopyJobType type = (Uri.TryCreate(destFileName, UriKind.Absolute, out Uri? uri) && !uri.IsFile) ? BackgroundCopyJobType.Upload : BackgroundCopyJobType.Download;
using BackgroundCopyJob mainJob = Jobs.Add("Temp" + Guid.NewGuid().ToString(), string.Empty, type);
using var manualReset = new ManualResetEventSlim(false);
BackgroundCopyException? err = null;
// Set event handlers for job, these are weak references.
mainJob.Completed += OnCompleted;
mainJob.Error += OnError;
mainJob.FileTransferred += OnFileTransferred;
mainJob.FileRangesTransferred += OnFileRangesTransferred;
add(mainJob.Files);
mainJob.Resume();
manualReset.Wait(ct);
var raiseException = false;
if (ct.IsCancellationRequested)
{
mainJob.Cancel();
raiseException = true;
}
// Remove weak references to prevent memory leak.
mainJob.FileRangesTransferred -= OnFileRangesTransferred;
mainJob.FileTransferred -= OnFileTransferred;
mainJob.Completed -= OnCompleted;
mainJob.Error -= OnError;
if (raiseException)
throw new OperationCanceledException();
if (null != err)
throw err;
// Better performance when event methods are defined seperately, preferably static.
void OnCompleted(object? s, BackgroundCopyJobEventArgs e)
{
if (s is BackgroundCopyJob job)
{
ReportProgress(job, BackgroundCopyJobState.Transferred);
job.Complete();
manualReset.Set();
}
}
void OnError(object? s, BackgroundCopyJobEventArgs e)
{
if (s is BackgroundCopyJob job)
{
err = job.LastError;
job.Cancel();
manualReset.Set();
}
}
void OnFileTransferred(object? s, BackgroundCopyFileTransferredEventArgs e)
{
if (s is BackgroundCopyJob job)
ReportProgress(job, job.State);
}
void OnFileRangesTransferred(object? s, BackgroundCopyFileRangesTransferredEventArgs e)
{
if (s is BackgroundCopyJob job)
ReportProgress(job, job.State);
}
void ReportProgress(BackgroundCopyJob job, BackgroundCopyJobState state) => report?.Invoke(state, job.Progress.PercentComplete);
}
}