BITS pinvoke updates, helper classes, and unit tests

pull/10/head
David Hall 2018-05-29 17:45:25 -06:00
parent 5d907ff3a3
commit 91f6d59f7f
13 changed files with 6097 additions and 200 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,64 @@
using System;
using System.Runtime.InteropServices;
using Vanara.InteropServices;
using Vanara.PInvoke;
using static Vanara.PInvoke.BITS;
namespace Vanara.IO
{
/// <summary>Exceptions specific to BITS</summary>
public class BackgroundCopyException : Exception
{
private HRESULT code;
private BG_ERROR_CONTEXT ctx;
private SafeCoTaskMemString ctxDesc, protocol;
private string errDesc;
private IBackgroundCopyFile iVal;
internal BackgroundCopyException(IBackgroundCopyError err)
{
const uint lang = 0x1; // LANG_NEUTRAL, SUBLANG_DEFAULT
err.GetError(out ctx, out code);
try { ctxDesc = err.GetErrorContextDescription(lang); }
catch { ctxDesc = SafeCoTaskMemString.Null; }
try { errDesc = err.GetErrorDescription(lang); }
catch { errDesc = SafeCoTaskMemString.Null; }
try { protocol = err.GetProtocol(); }
catch { protocol = SafeCoTaskMemString.Null; }
iVal = err.GetFile();
}
internal BackgroundCopyException(COMException cex)
{
code = cex.ErrorCode;
errDesc = BackgroundCopyManager.GetErrorMessage(code);
if (errDesc == null)
code.ThrowIfFailed();
}
/// <summary>Context in which the error occurred.</summary>
public BackgroundCopyErrorContext Context => (BackgroundCopyErrorContext)ctx;
/// <summary>Description of the context in which the error occurred.</summary>
public string ContextDescription => ctxDesc;
/// <summary>Gets the error code.</summary>
/// <value>The error code.</value>
public HRESULT ErrorCode => code;
/// <summary>If error was related to a file, returns information about the file and its progress. Otherwise, returns NULL.</summary>
public BackgroundCopyFileInfo File
{
get { if (iVal == null) return null; return new BackgroundCopyFileInfo(iVal); }
}
/// <summary>The error text associated with the error.</summary>
public override string Message => errDesc;
/// <summary>
/// Contains the protocol used to transfer the file. The string contains "http" for the HTTP protocol and "file" for the SMB protocol and to NULL if the
/// error is not related to the transfer protocol.
/// </summary>
public string Protocol => protocol;
}
}

View File

@ -0,0 +1,268 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Vanara.PInvoke;
using static Vanara.PInvoke.BITS;
namespace Vanara.IO
{
/// <summary>Manages the set of files for a background copy job.</summary>
public class BackgroundCopyFileCollection : ICollection<BackgroundCopyFileInfo>, IDisposable
{
private IBackgroundCopyJob m_ijob;
internal BackgroundCopyFileCollection(IBackgroundCopyJob ijob)
{
m_ijob = ijob;
}
internal BackgroundCopyFileCollection()
{
}
/// <summary>Gets the number of files in the current job.</summary>
public int Count
{
get
{
try
{
return (int)m_ijob.EnumFiles().GetCount();
}
catch (COMException cex)
{
HandleCOMException(cex);
}
return 0;
}
}
/// <summary>Gets a value indicating whether the <see cref="ICollection{T}"/> is read-only.</summary>
bool ICollection<BackgroundCopyFileInfo>.IsReadOnly => false;
/// <summary>Add a file to a download or an upload job. Only one file can be added to upload jobs.</summary>
/// <param name="remoteFilePath">
/// Contains the name of the file on the server (for example, http://[server]/[path]/file.ext). The format of the name must conform to the transfer
/// protocol you use. You cannot use wildcards in the path or file name. The URL must contain only legal URL characters; no escape processing is
/// performed. The URL is limited to 2,200 characters, not including the null terminator. You can use SMB to express the remote name of the file to
/// download or upload; there is no SMB support for upload-reply jobs. You can specify the remote name as a UNC path, full path with a network drive, or
/// use the "file://" prefix.
/// </param>
/// <param name="localFilePath">
/// Contains the name of the file on the client. The file name must include the full path (for example, d:\myapp\updates\file.ext). You cannot use
/// wildcards in the path or file name, and directories in the path must exist. The user must have permission to write to the local directory for
/// downloads and the reply portion of an upload-reply job. BITS does not support NTFS streams. Instead of using network drives, which are session
/// specific, use UNC paths.
/// </param>
public void Add(string remoteFilePath, string localFilePath)
{
try
{
m_ijob.AddFile(remoteFilePath, localFilePath);
}
catch (COMException cex)
{
HandleCOMException(cex);
}
}
/// <summary>Add a file to a download job and specify the ranges of the file you want to download.</summary>
/// <param name="remoteFilePath">
/// Contains the name of the file on the server (for example, http://[server]/[path]/file.ext). The format of the name must conform to the transfer
/// protocol you use. You cannot use wildcards in the path or file name. The URL must contain only legal URL characters; no escape processing is
/// performed. The URL is limited to 2,200 characters, not including the null terminator. You can use SMB to express the remote name of the file to
/// download or upload; there is no SMB support for upload-reply jobs. You can specify the remote name as a UNC path, full path with a network drive, or
/// use the "file://" prefix.
/// </param>
/// <param name="localFilePath">
/// Contains the name of the file on the client. The file name must include the full path (for example, d:\myapp\updates\file.ext). You cannot use
/// wildcards in the path or file name, and directories in the path must exist. The user must have permission to write to the local directory for
/// downloads and the reply portion of an upload-reply job. BITS does not support NTFS streams. Instead of using network drives, which are session
/// specific, use UNC paths.
/// </param>
/// <param name="initialOffset">Zero-based offset to the beginning of the range of bytes to download from a file.</param>
/// <param name="length">Number of bytes in the range.</param>
public void Add(string remoteFilePath, string localFilePath, long initialOffset, long length = -1)
{
IBackgroundCopyJob3 ijob3 = null;
try { ijob3 = (IBackgroundCopyJob3)m_ijob; }
catch { throw new NotSupportedException(); }
var rng = new[] { new BG_FILE_RANGE { InitialOffset = (ulong)initialOffset, Length = length == -1 ? ulong.MaxValue : (ulong)length } };
try
{
ijob3.AddFileWithRanges(remoteFilePath, localFilePath, 1, rng);
}
catch (COMException cex)
{
HandleCOMException(cex);
}
}
/// <summary>Add a list of files to download from a URL.</summary>
/// <param name="remoteUrlRoot">
/// Contains the name of the directory on the server (for example, http://[server]/[path]/). The format of the name must conform to the transfer protocol
/// you use. You cannot use wildcards in the path or file name. The URL must contain only legal URL characters; no escape processing is performed. The
/// URL is limited to 2,200 characters, not including the null terminator. You can use SMB to express the remote name of the file to download or upload;
/// there is no SMB support for upload-reply jobs. You can specify the remote name as a UNC path, full path with a network drive, or use the "file://" prefix.
/// </param>
/// <param name="localDirectory">
/// Contains the name of the directory on the client. The directory must exist. The user must have permission to write to the directory for downloads.
/// BITS does not support NTFS streams. Instead of using network drives, which are session specific, use UNC paths.
/// </param>
/// <param name="files">List of file names to retrieve. Filename will be appended to both the remoteUrlRoot and the localDirectory.</param>
public void AddRange(Uri remoteUrlRoot, DirectoryInfo localDirectory, IEnumerable<string> files)
{
var array = files.Select(s => new BG_FILE_INFO(new Uri(remoteUrlRoot, s).AbsoluteUri, Path.Combine(localDirectory.FullName, s))).ToArray();
try
{
m_ijob.AddFileSet((uint)array.Length, array);
}
catch (COMException cex)
{
HandleCOMException(cex);
}
}
/// <summary>Disposes of the BackgroundCopyFileSet object.</summary>
void IDisposable.Dispose()
{
m_ijob = null;
}
/// <summary>
/// Returns an object that implements the <see cref="IEnumerator"/> interface and that can iterate through the <see cref="BackgroundCopyFileInfo"/>
/// objects within the <see cref="BackgroundCopyFileCollection"/> collection.
/// </summary>
/// <returns>
/// Returns an object that implements the <see cref="IEnumerator"/> interface and that can iterate through the <see cref="BackgroundCopyFileInfo"/>
/// objects within the <see cref="BackgroundCopyFileCollection"/> collection.
/// </returns>
public IEnumerator<BackgroundCopyFileInfo> GetEnumerator()
{
var ienum = m_ijob.EnumFiles();
return new Enumerator(ienum);
}
/// <summary>Adds an item to the <see cref="ICollection{T}"/>.</summary>
/// <param name="item">The object to add to the <see cref="ICollection{T}"/>.</param>
/// <exception cref="NotImplementedException"></exception>
void ICollection<BackgroundCopyFileInfo>.Add(BackgroundCopyFileInfo item)
{
}
/// <summary>Removes all items from the <see cref="ICollection{T}"/>.</summary>
/// <exception cref="NotImplementedException"></exception>
void ICollection<BackgroundCopyFileInfo>.Clear()
{
throw new NotSupportedException();
}
/// <summary>Determines whether the <see cref="ICollection{T}"/> contains a specific value.</summary>
/// <param name="item">The object to locate in the <see cref="ICollection{T}"/>.</param>
/// <returns>true if <paramref name="item"/> is found in the <see cref="ICollection{T}"/>; otherwise, false.</returns>
/// <exception cref="NotImplementedException"></exception>
bool ICollection<BackgroundCopyFileInfo>.Contains(BackgroundCopyFileInfo item)
{
throw new NotSupportedException();
}
/// <summary>
/// Copies the elements of the <see cref="ICollection{T}"/> to an <see cref="T:System.Array"/>, starting at a particular <see cref="T:System.Array"/> index.
/// </summary>
/// <param name="array">
/// The one-dimensional <see cref="T:System.Array"/> that is the destination of the elements copied from <see cref="ICollection{T}"/>. The
/// <see cref="T:System.Array"/> must have zero-based indexing.
/// </param>
/// <param name="arrayIndex">The zero-based index in <paramref name="array"/> at which copying begins.</param>
/// <exception cref="NotImplementedException"></exception>
void ICollection<BackgroundCopyFileInfo>.CopyTo(BackgroundCopyFileInfo[] array, int arrayIndex)
{
throw new NotSupportedException();
}
/// <summary>Returns an enumerator that iterates through a collection.</summary>
/// <returns>An <see cref="IEnumerator"/> object that can be used to iterate through the collection.</returns>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary>Removes the first occurrence of a specific object from the <see cref="ICollection{T}"/>.</summary>
/// <param name="item">The object to remove from the <see cref="ICollection{T}"/>.</param>
/// <returns>
/// true if <paramref name="item"/> was successfully removed from the <see cref="ICollection{T}"/>; otherwise, false. This method also returns false if
/// <paramref name="item"/> is not found in the original <see cref="ICollection{T}"/>.
/// </returns>
/// <exception cref="NotImplementedException"></exception>
bool ICollection<BackgroundCopyFileInfo>.Remove(BackgroundCopyFileInfo item)
{
throw new NotSupportedException();
}
internal BackgroundCopyFileInfo[] GetBCFIArray() => this.ToArray();
private void HandleCOMException(COMException cex)
{
var state = m_ijob.GetState();
if (state == BG_JOB_STATE.BG_JOB_STATE_ERROR || state == BG_JOB_STATE.BG_JOB_STATE_TRANSIENT_ERROR)
{
var pErr = m_ijob.GetError();
throw new BackgroundCopyException(pErr);
}
else
throw new BackgroundCopyException(cex);
}
/// <summary>
/// An implementation the <see cref="IEnumerator"/> interface that can iterate through the <see cref="BackgroundCopyFileInfo"/> objects within the
/// <see cref="BackgroundCopyFileCollection"/> collection.
/// </summary>
private sealed class Enumerator : IEnumerator<BackgroundCopyFileInfo>
{
private IBackgroundCopyFile icurrentfile;
private IEnumBackgroundCopyFiles ienum;
internal Enumerator(IEnumBackgroundCopyFiles enumfiles)
{
ienum = enumfiles;
ienum.Reset();
}
/// <summary>
/// Gets the <see cref="BackgroundCopyFileInfo"/> object in the <see cref="BackgroundCopyFileCollection"/> collection to which the enumerator is pointing.
/// </summary>
public BackgroundCopyFileInfo Current => icurrentfile != null ? new BackgroundCopyFileInfo(icurrentfile) : throw new InvalidOperationException();
/// <summary>
/// Gets the <see cref="BackgroundCopyFileInfo"/> object in the <see cref="BackgroundCopyFileCollection"/> collection to which the enumerator is pointing.
/// </summary>
object IEnumerator.Current => Current;
/// <summary>Disposes of the Enumerator object.</summary>
public void Dispose()
{
ienum = null;
icurrentfile = null;
}
/// <summary>Moves the enumerator index to the next object in the collection.</summary>
/// <returns></returns>
public bool MoveNext()
{
try
{
icurrentfile = ienum.Next(1)?.FirstOrDefault();
return icurrentfile != null;
}
catch { return false; }
}
/// <summary>Resets the enumerator index to the beginning of the <see cref="BackgroundCopyFileCollection"/> collection.</summary>
public void Reset()
{
icurrentfile = null;
ienum.Reset();
}
}
}
}

View File

@ -0,0 +1,186 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using static Vanara.PInvoke.BITS;
namespace Vanara.IO
{
/// <summary>Information about a file in a background copy job.</summary>
public class BackgroundCopyFileInfo
{
internal BG_FILE_INFO fi;
private IBackgroundCopyFile iFile;
internal BackgroundCopyFileInfo(IBackgroundCopyFile ibgfile)
{
iFile = ibgfile;
}
internal BackgroundCopyFileInfo(BG_FILE_INFO bgfi)
{
fi = bgfi;
}
/// <summary>Retrieves the ranges that you want to download from the remote file.</summary>
/// <value>The file copy ranges.</value>
public IEnumerable<BackgroundCopyFileRange> FileRanges
{
get
{
IFile2.GetFileRanges(out var cnt, out var rng);
return rng.ToEnumerable<BackgroundCopyFileRange>((int)cnt);
}
}
/// <summary>Returns the set of file ranges that have been downloaded.</summary>
/// <value>The file copy ranges that have been downloaded. Ranges will be merged together as much as possible. The ranges are ordered by offset.</value>
public IEnumerable<BackgroundCopyFileRange> FilledFileRanges
{
get
{
IFile6.GetFilledFileRanges(out var cnt, out var rng);
return rng.ToEnumerable<BackgroundCopyFileRange>((int)cnt);
}
}
/// <summary>Gets or sets a value indicating whether the contents of the file are valid.</summary>
/// <value><c>true</c> if the file content is valid; otherwise, <c>false</c>.</value>
public bool IsFileContentValid { get => IFile3.GetValidationState(); set => IFile3.SetValidationState(value); }
/// <summary>
/// Size of the file in bytes. If the value is -1, the total size of the file has not been determined. BITS does not set this value if it cannot
/// determine the size of the file. For example, if the specified file or server does not exist, BITS cannot determine the size of the file. If you are
/// downloading ranges from a file, BytesTotal reflects the total number of bytes you want to download from the file.
/// </summary>
public long Length => (long)CopyProgress.BytesTotal;
/// <summary>Retrieves the local name of the file.</summary>
public string LocalFilePath
{
get => iFile == null ? fi.LocalName : iFile.GetLocalName();
set
{
if (iFile != null)
throw new InvalidOperationException("You cannot change the LocalFilePath property on CurrentFileSet results.");
fi.LocalName = value;
}
}
/// <summary>Gets the amount of file data downloaded from the originating server.</summary>
public ulong OriginServerDownloadBytes { get { IFile4.GetPeerDownloadStats(out var o, out var _); return o; } }
/// <summary>Gets the amount of file data downloaded from a peer-to-peer source.</summary>
public ulong PeerDownloadBytes { get { IFile4.GetPeerDownloadStats(out var _, out var p); return p; } }
/// <summary>Gets the percentage of the transfer that has completed.</summary>
public float PercentComplete
{
get
{
var p = CopyProgress;
return p.Completed || p.BytesTotal == 0 ? 100.0F : p.BytesTransferred * 100.0F / p.BytesTotal;
}
}
/// <summary>Retrieves the remote name of the file.</summary>
public string RemoteFilePath
{
get => iFile == null ? fi.RemoteName : iFile.GetRemoteName();
set
{
fi.RemoteName = value;
(iFile as IBackgroundCopyFile2)?.SetRemoteName(value);
}
}
public string ResponseHeaders
{
get
{
var hdr = IFile5.GetProperty(BITS_FILE_PROPERTY_ID.BITS_FILE_PROPERTY_ID_HTTP_RESPONSE_HEADERS);
return hdr.String;
}
set
{
var hdr = new BITS_FILE_PROPERTY_VALUE { String = value };
IFile5.SetProperty(BITS_FILE_PROPERTY_ID.BITS_FILE_PROPERTY_ID_HTTP_RESPONSE_HEADERS, ref hdr);
}
}
/// <summary>Gets the full path of the temporary file that contains the content of the download.</summary>
public string TemporaryName => IFile3.GetTemporaryName();
/// <summary>
/// For downloads, the value is TRUE if the file is available to the user; otherwise, the value is FALSE. Files are available to the user after calling
/// the <see cref="BackgroundCopyJob.Complete"/> method. If the <see cref="BackgroundCopyJob.Complete"/> method generates a transient error, those files
/// processed before the error occurred are available to the user; the others are not. Use the Completed property to determine if the file is available
/// to the user when Complete fails. For uploads, the value is TRUE when the file upload is complete; otherwise, the value is FALSE.
/// </summary>
public bool TransferCompleted => CopyProgress.Completed;
/// <summary>Gets the number of bytes transferred.</summary>
public long BytesTransferred => (long)CopyProgress.BytesTransferred;
/// <summary>Gets a value that determines if any part of the file was downloaded from a peer.</summary>
/// <value>Is TRUE if any part of the file was downloaded from a peer; otherwise, FALSE.</value>
public bool IsDownloadedFromPeer => IFile3.IsDownloadedFromPeer();
/// <summary>Retrieves the progress of the file transfer.</summary>
internal BG_FILE_PROGRESS CopyProgress => iFile?.GetProgress() ?? throw new InvalidOperationException("You can only get the CopyProgress on CurrentFileSet results.");
private IBackgroundCopyFile2 IFile2 => GetDerived<IBackgroundCopyFile2>();
private IBackgroundCopyFile3 IFile3 => GetDerived<IBackgroundCopyFile3>();
private IBackgroundCopyFile4 IFile4 => GetDerived<IBackgroundCopyFile4>();
private IBackgroundCopyFile5 IFile5 => GetDerived<IBackgroundCopyFile5>();
private IBackgroundCopyFile6 IFile6 => GetDerived<IBackgroundCopyFile6>();
/// <summary>Adds a new set of file ranges to be prioritized for download.</summary>
/// <param name="ranges">
/// An array of file ranges to be downloaded. Requested ranges are allowed to overlap previously downloaded (or pending) ranges. Ranges are automatically
/// split into non-overlapping ranges.
/// </param>
public void RequestFileRanges(BackgroundCopyFileRange[] ranges)
{
IFile6.RequestFileRanges((uint)ranges.Length, Array.ConvertAll(ranges, r => r.fr));
}
/// <summary>Returns a <see cref="System.String" /> that represents this instance.</summary>
/// <returns>A <see cref="System.String" /> that represents this instance.</returns>
public override string ToString() => $"{LocalFilePath} => {RemoteFilePath}";
/// <summary>Specifies a position to prioritize downloading missing data from.</summary>
/// <param name="offset">Specifies the new position to prioritize downloading missing data from.</param>
public void UpdateDownloadPosition(ulong offset) => IFile6.UpdateDownloadPosition(offset);
private T GetDerived<T>() where T : class
{
T ret = iFile as T;
return ret ?? throw new PlatformNotSupportedException();
}
}
/// <summary>Identifies a range of bytes to download from a file.</summary>
[StructLayout(LayoutKind.Sequential)]
public class BackgroundCopyFileRange
{
internal BG_FILE_RANGE fr;
/// <summary>Zero-based offset to the beginning of the range of bytes to download from a file.</summary>
public ulong InitialOffset { get => fr.InitialOffset; set => fr.InitialOffset = value; }
/// <summary>
/// The length of the range, in bytes. Do not specify a zero byte length. To indicate that the range extends to the end of the file, specify <c>BG_LENGTH_TO_EOF</c>.
/// </summary>
public ulong Length { get => fr.Length; set => fr.Length = value; }
/// <summary>Performs an implicit conversion from <see cref="BG_FILE_RANGE"/> to <see cref="BackgroundCopyFileRange"/>.</summary>
/// <param name="p">The BG_FILE_RANGE instance.</param>
/// <returns>The result of the conversion.</returns>
public static implicit operator BackgroundCopyFileRange(BG_FILE_RANGE p) => new BackgroundCopyFileRange { fr = p };
}
}

View File

@ -0,0 +1,774 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Vanara.Extensions;
using Vanara.InteropServices;
using static Vanara.PInvoke.BITS;
namespace Vanara.IO
{
/// <summary>
/// Provides job-related progress information, such as the number of bytes and files transferred. For upload jobs, the progress applies to the upload file,
/// not the reply file.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 8)]
public struct BackgroundCopyJobProgress
{
internal BG_JOB_PROGRESS prog;
/// <summary>
/// <para>
/// Total number of bytes to transfer for all files in the job. If the value UInt64.MaxValue, the total size of all files in the job has not been
/// determined. BITS does not set this value if it cannot determine the size of one of the files. For example, if the specified file or server does not
/// exist, BITS cannot determine the size of the file.
/// </para>
/// <para>If you are downloading ranges from the file, <c>BytesTotal</c> includes the total number of bytes you want to download from the file.</para>
/// </summary>
public ulong BytesTotal { get => prog.BytesTotal; set => prog.BytesTotal = value; }
/// <summary>Number of bytes transferred.</summary>
public ulong BytesTransferred { get => prog.BytesTransferred; set => prog.BytesTransferred = value; }
/// <summary>Total number of files to transfer for this job.</summary>
public uint FilesTotal { get => prog.FilesTotal; set => prog.FilesTotal = value; }
/// <summary>Number of files transferred.</summary>
public uint FilesTransferred { get => prog.FilesTransferred; set => prog.FilesTransferred = value; }
/// <summary>Gets the percent of total bytes transferred represented as a number between 0 and 100.</summary>
public byte PercentComplete => (byte)(BytesTotal == 0 ? 0f : BytesTransferred * 100f / BytesTotal);
internal BackgroundCopyJobProgress(BG_JOB_PROGRESS p) { prog = p; }
/// <summary>Performs an implicit conversion from <see cref="BG_JOB_PROGRESS"/> to <see cref="BackgroundCopyJobProgress"/>.</summary>
/// <param name="p">The BG_JOB_PROGRESS instance.</param>
/// <returns>The result of the conversion.</returns>
public static implicit operator BackgroundCopyJobProgress(BG_JOB_PROGRESS p) => new BackgroundCopyJobProgress(p);
}
/// <summary>Provides progress information related to the reply portion of an upload-reply job.</summary>
[StructLayout(LayoutKind.Sequential, Pack = 8)]
public struct BackgroundCopyJobReplyProgress
{
internal BG_JOB_REPLY_PROGRESS prog;
/// <summary>Size of the file in bytes. The value is <c>UInt64.MaxValue</c> if the reply has not begun.</summary>
public ulong BytesTotal { get => prog.BytesTotal; set => prog.BytesTotal = value; }
/// <summary>Number of bytes transferred.</summary>
public ulong BytesTransferred { get => prog.BytesTransferred; set => prog.BytesTransferred = value; }
internal BackgroundCopyJobReplyProgress(BG_JOB_REPLY_PROGRESS p) { prog = p; }
/// <summary>Performs an implicit conversion from <see cref="BG_JOB_REPLY_PROGRESS"/> to <see cref="BackgroundCopyJobReplyProgress"/>.</summary>
/// <param name="p">The BG_JOB_REPLY_PROGRESS instance.</param>
/// <returns>The result of the conversion.</returns>
public static implicit operator BackgroundCopyJobReplyProgress(BG_JOB_REPLY_PROGRESS p) => new BackgroundCopyJobReplyProgress(p);
}
/// <summary>Used by <see cref="BackgroundCopyJob.FileRangesTransferred"/> events.</summary>
public class BackgroundCopyFileRangesTransferredEventArgs : BackgroundCopyFileTransferredEventArgs
{
internal BackgroundCopyFileRangesTransferredEventArgs(BackgroundCopyJob job, IBackgroundCopyFile file, BG_FILE_RANGE[] ranges) : base(job, file)
{
Ranges = Array.ConvertAll(ranges, r => (BackgroundCopyFileRange)r);
}
/// <summary>
/// An array of the files ranges that have transferred since the last call to FileRangesTransferred or the last call to the RequestFileRanges method.
/// </summary>
public BackgroundCopyFileRange[] Ranges { get; private set; }
}
/// <summary>Used by <see cref="BackgroundCopyJob.FileTransferred"/> events.</summary>
public class BackgroundCopyFileTransferredEventArgs : BackgroundCopyJobEventArgs
{
internal BackgroundCopyFileTransferredEventArgs(BackgroundCopyJob job, IBackgroundCopyFile pFile) : base(job)
{
FileInfo = new BackgroundCopyFileInfo(pFile);
}
/// <summary>A BackgroundCopyFileInfo object that contains information about the file.</summary>
public BackgroundCopyFileInfo FileInfo { get; private set; }
}
/// <summary>A job in the Backgroup Copy Service (BITS)</summary>
public class BackgroundCopyJob : IDisposable
{
internal static readonly TimeSpan DEFAULT_RETRY_DELAY = TimeSpan.FromSeconds(600); //10 minutes (600 seconds)
internal static readonly TimeSpan DEFAULT_RETRY_PERIOD = TimeSpan.FromSeconds(1209600); //20160 minutes (1209600 seconds)
internal static readonly TimeSpan DEFAULT_TIMEOUT = TimeSpan.FromSeconds(7776000); // 7776000 seconds
private IBackgroundCopyJob m_ijob;
private Notifier m_notifier;
internal BackgroundCopyJob(IBackgroundCopyJob ijob)
{
m_ijob = ijob ?? throw new ArgumentNullException(nameof(ijob));
m_notifier = new Notifier(this);
m_ijob.SetNotifyInterface(m_notifier);
NotifyFlags = BG_NOTIFY.BG_NOTIFY_FILE_RANGES_TRANSFERRED | BG_NOTIFY.BG_NOTIFY_FILE_TRANSFERRED | BG_NOTIFY.BG_NOTIFY_JOB_ERROR | BG_NOTIFY.BG_NOTIFY_JOB_MODIFICATION | BG_NOTIFY.BG_NOTIFY_JOB_TRANSFERRED;
Files = new BackgroundCopyFileCollection(m_ijob);
Credentials = new BackgroundCopyJobCredentials(IJob2);
}
/// <summary>Occurs when all of the files in the job have been transferred.</summary>
public event EventHandler<BackgroundCopyJobEventArgs> Completed;
/// <summary>Fires when an error occurs.</summary>
public event EventHandler<BackgroundCopyJobEventArgs> Error;
/// <summary>Occurs when file ranges have been transferred.</summary>
public event EventHandler<BackgroundCopyFileRangesTransferredEventArgs> FileRangesTransferred;
/// <summary>Occurs when a file has been transferred.</summary>
public event EventHandler<BackgroundCopyFileTransferredEventArgs> FileTransferred;
/// <summary>
/// Occurs when the job has been modified. For example, a property value changed, the state of the job changed, or progress is made transferring the files.
/// </summary>
public event EventHandler<BackgroundCopyJobEventArgs> Modified;
/// <summary>Gets or sets the flags that identify the owner and ACL information to maintain when transferring a file using SMB.</summary>
public BackgroundCopyACLFlags ACLFlags
{
get => RunAction(() => (BackgroundCopyACLFlags)IJob3.GetFileACLFlags(), BackgroundCopyACLFlags.None);
set => RunAction(() => IJob3.SetFileACLFlags((BG_COPY_FILE)value));
}
/// <summary>Retrieves the client certificate from the job.</summary>
public X509Certificate2 Certificate
{
get
{
IHttpOp.GetClientCertificate(out BG_CERT_STORE_LOCATION loc, out var mstore, out var blob, out var subj);
if (blob.IsInvalid) return null;
string store = mstore;
switch (store)
{
case "MY":
store = "My";
break;
case "ROOT":
store = "Root";
break;
case "SPC":
store = "TrustedPublisher";
break;
case "CA":
default:
break;
}
var xstore = new X509Store(store, (StoreLocation)(loc + 1));
xstore.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
return xstore.Certificates.Find(X509FindType.FindBySubjectName, (string)subj, false).OfType<X509Certificate2>().FirstOrDefault() ??
new X509Certificate2(blob.ToArray<byte>(20));
}
}
/// <summary>Gets the time the job was created.</summary>
public DateTime CreationTime => Times.CreationTime.ToDateTime();
/// <summary>The credentials to use for a proxy or remote server user authentication request.</summary>
public BackgroundCopyJobCredentials Credentials { get; private set; }
/// <summary>Gets or sets one or more custom HTTP headers to include in HTTP requests.</summary>
public System.Net.WebHeaderCollection CustomHeaders
{
get
{
var hdr = new System.Net.WebHeaderCollection();
var str = RunAction(() => IHttpOp.GetCustomHeaders().ToString(), null);
if (str != null)
{
foreach (var s in str.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries))
hdr.Add(s);
}
return hdr;
}
set => RunAction(() => IHttpOp.SetCustomHeaders(value.Count == 0 ? null : string.Join("\n", value.AllKeys.Select(k => $"{k}:{value[k]}").ToArray())));
}
/// <summary>Gets or sets the description of the job.</summary>
[DefaultValue("")]
public string Description
{
get => RunAction(() => m_ijob.GetDescription(), string.Empty);
set => RunAction(() => m_ijob.SetDescription(value));
}
/// <summary>Gets or sets a value indicating whether notifications are disabled.</summary>
/// <value><c>true</c> if notifications are disabled; otherwise, <c>false</c>.</value>
[DefaultValue(false)]
public bool DisableNotifications
{
get => NotifyFlags.IsFlagSet(BG_NOTIFY.BG_NOTIFY_DISABLE);
set => NotifyFlags = NotifyFlags.SetFlags(BG_NOTIFY.BG_NOTIFY_DISABLE, value);
}
/// <summary>Gets or sets the display name of the job.</summary>
public string DisplayName
{
get => RunAction(() => m_ijob.GetDisplayName(), string.Empty);
set => RunAction(() => m_ijob.SetDisplayName(value));
}
/// <summary>Marks a BITS job as being willing to download content which does not support the normal HTTP requirements for BITS downloads: HEAD requests, the Content-Length header, and the Content-Range header. Downloading this type of content is opt-in, because BITS cannot pause and resume downloads jobs without that support. If a job with this property enabled is interrupted for any reason, such as a temporary loss of network connectivity or the system rebooting, BITS will restart the download from the beginning instead of resuming where it left off. BITS also cannot throttle bandwidth usage for dynamic downloads; BITS will not perform unthrottled transfers for any job that does not have BG_JOB_PRIORITY_FOREGROUND assigned, so you should typically set that priority every time you use set a job as allowing dynamic content.
/// <para>This property is only supported for BG_JOB_TYPE_DOWNLOAD jobs. It is not supported for downloads that use FILE_RANGES. This property may only be set prior to the first time Resume is called on a job.</para></summary>
[DefaultValue(false)]
public bool DynamicContent
{
get => (bool)GetProperty(BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_DYNAMIC_CONTENT);
set => SetProperty(BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_DYNAMIC_CONTENT, value);
}
/// <summary>Gets the number of errors that have occured in this job.</summary>
public int ErrorCount => RunAction(() => (int)m_ijob.GetErrorCount());
/// <summary>Manages the files that are a part of this job.</summary>
public BackgroundCopyFileCollection Files { get; private set; }
/// <summary>Marks a BITS job as not requiring strong reliability guarantees. Enabling this property will cause BITS to avoid persisting information about normal job progress, which BITS normally does periodically. In the event of an unexpected shutdown, such as a power loss, during a transfer, this will cause BITS to lose progress and restart the job from the beginning instead of resuming from where it left off as usual. However, it will also reduce the number of disk writes BITS makes over the course of a jobs lifetime, which can improve performance for smaller jobs.
/// <para>This property also causes BITS to download directly into the destination file, instead of downloading to a temporary file and moving the temporary file to the final destination once the transfer is complete.This means that BITS will not clean up any partially downloaded content if a job is cancelled or encounters a fatal error condition; the BITS caller is responsible for cleaning up the destination file, if it gets created.However, it will also slightly reduce disk overhead.</para>
/// <para>This property is only recommended for scenarios which involve high numbers of small jobs(under 1MB) and which do not require reliability to power loss or other unexpected shutdown events.The performance savings are not generally significant for small numbers of jobs or for larger jobs.</para>
/// <para>This property is only supported for BG_JOB_TYPE_DOWNLOAD jobs. This property may only be set prior to adding any files to a job.</para></summary>
[DefaultValue(false)]
public bool HighPerformance
{
get => (bool)GetProperty(BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_HIGH_PERFORMANCE);
set => SetProperty(BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_HIGH_PERFORMANCE, value);
}
/// <summary>Gets the job identifier.</summary>
public Guid ID => RunAction(() => m_ijob.GetId(), Guid.Empty);
/// <summary>Gets the type of job, such as download.</summary>
public BackgroundCopyJobType JobType => RunAction(() => (BackgroundCopyJobType)m_ijob.GetType(), BackgroundCopyJobType.Download);
/// <summary>Gets the last exeception that occured in the job.</summary>
public BackgroundCopyException LastError
{
get
{
var state = State;
if (state != BackgroundCopyJobState.Error && state != BackgroundCopyJobState.TransientError)
return null;
var err = RunAction(() => m_ijob.GetError());
return err == null ? null : new BackgroundCopyException(err);
}
}
/// <summary>The ID for marking the maximum number of bytes a BITS job will be allowed to download in total. This property is intended for use with BITS_JOB_PROPERTY_DYNAMIC_CONTENT, where you may not be able to determine the size of the file to be downloaded ahead of time but would like to cap the total possible download size.
/// <para>This property is only supported for BG_JOB_TYPE_DOWNLOAD jobs. This property may only be set prior to the first time Resume is called on a job.</para></summary>
[DefaultValue(0)]
public ulong MaxDownloadSize
{
get => (ulong)GetProperty(BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_MAX_DOWNLOAD_SIZE);
set => SetProperty(BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_MAX_DOWNLOAD_SIZE, value);
}
/// <summary>Sets the maximum time that BITS will spend transferring the files in the job.</summary>
/// <value>Maximum time, in seconds, that BITS will spend transferring the files in the job. The default is 7,776,000 seconds (90 days).</value>
[DefaultValue(typeof(TimeSpan), "90.00:00:00")]
public TimeSpan MaximumDownloadTime
{
get => RunAction(() => TimeSpan.FromSeconds((int)IJob4.GetMaximumDownloadTime()), DEFAULT_TIMEOUT);
set => RunAction(() => IJob4.SetMaximumDownloadTime((uint)value.TotalSeconds));
}
/// <summary>Used to control the timing of BITS JobNotification and FileRangesTransferred notifications. Enabling this property lets a user be notified at a different rate. This property may be changed while a transfer is ongoing; however, the new rate may not be applied immediately. The default value is 500 milliseconds.</summary>
[DefaultValue(typeof(TimeSpan), "00:00:00")]
public TimeSpan MinimumNotificationInterval
{
get => TimeSpan.FromMilliseconds((uint)GetProperty(BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_MINIMUM_NOTIFICATION_INTERVAL_MS));
set => SetProperty(BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_MINIMUM_NOTIFICATION_INTERVAL_MS, (uint)value.TotalMilliseconds);
}
/// <summary>
/// Gets or sets the minimum length of time, in seconds, that BITS waits after encountering a transient error before trying to transfer the file. The
/// default retry delay is 600 seconds (10 minutes). The minimum retry delay that you can specify is 60 seconds. If you specify a value less than 60
/// seconds, BITS changes the value to 60 seconds.
/// </summary>
[DefaultValue(typeof(TimeSpan), "00:10:00")]
public TimeSpan MinimumRetryDelay
{
get => RunAction(() => TimeSpan.FromSeconds((int)m_ijob.GetMinimumRetryDelay()), DEFAULT_RETRY_DELAY);
set => RunAction(() => m_ijob.SetMinimumRetryDelay((uint)value.TotalSeconds));
}
/// <summary>Gets the time the job was last modified or bytes were transferred.</summary>
public DateTime ModificationTime => Times.ModificationTime.ToDateTime();
/// <summary>
/// Gets or sets the length of time, in seconds, that BITS tries to transfer the file after the first transient error occurs. The default retry period is
/// 1,209,600 seconds (14 days). Set the retry period to 0 to prevent retries and to force the job into the Error state for all errors.
/// </summary>
[DefaultValue(typeof(TimeSpan), "14.00:00:00")]
public TimeSpan NoProgressTimeout
{
get => RunAction(() => TimeSpan.FromSeconds((int)m_ijob.GetNoProgressTimeout()), DEFAULT_RETRY_PERIOD);
set => RunAction(() => m_ijob.SetNoProgressTimeout((uint)value.TotalSeconds));
}
/// <summary>Used to register a COM callback by CLSID to receive notifications about the progress and completion of a BITS job. The CLSID must refer to a class associated with a registered out-of-process COM server. It may also be set to GUID_NULL to clear a previously set notification CLSID.</summary>
[DefaultValue(typeof(Guid), "00000000000000000000000000000000")]
public Guid NotificationCLSID
{
get => (Guid)GetProperty(BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_NOTIFICATION_CLSID);
set => SetProperty(BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_NOTIFICATION_CLSID, value);
}
/// <summary>
/// Gets or sets the program to execute if the job enters the Error or Transferred state. BITS executes the program in the context of the user who called
/// this method.
/// </summary>
[DefaultValue(null)]
public string NotifyProgram
{
get
{
SafeCoTaskMemString p, a;
try
{
IJob2.GetNotifyCmdLine(out p, out a);
}
catch
{
return string.Empty;
}
if (string.IsNullOrEmpty(a))
{
if (p == null)
return string.Empty;
else
return p;
}
return string.Format("\"{0}\" {1}", p, a);
}
set
{
string p = value, a = string.Empty;
if (string.IsNullOrEmpty(value))
p = a = null;
else
{
if (value[0] == '"')
{
int i = p.IndexOf('"', 1);
if (i + 3 <= p.Length)
a = p.Substring(i + 2);
p = p.Substring(1, i - 1);
}
else
{
int i = p.IndexOf(' ');
if (i + 2 <= p.Length)
a = p.Substring(i + 1);
p = p.Substring(0, i);
}
}
RunAction(() => IJob2.SetNotifyCmdLine(p, a));
}
}
/// <summary>The ID that is used to control whether a job is in On Demand mode. On Demand jobs allow the app to request particular ranges for a file download instead of downloading from the start to the end. The default value is FALSE; the job is not on-demand. Ranges are requested using the IBackgroundCopyFile6::RequestFileRanges method.
/// <para>The requirements for a BITS_JOB_PROPERTY_ON_DEMAND_MODE job is that the transfer must be a BG_JOB_TYPE_DOWNLOAD job. The job must not be DYNAMIC and the server must be an HTTP or HTTPS server and the server requirements for range support must all be met.</para></summary>
[DefaultValue(false)]
public bool OnDemand
{
get => (bool)GetProperty(BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_ON_DEMAND_MODE);
set => SetProperty(BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_ON_DEMAND_MODE, value);
}
/// <summary>Retrieve the identity of the job's owner.</summary>
public System.Security.Principal.SecurityIdentifier Owner => RunAction(() => new System.Security.Principal.SecurityIdentifier(m_ijob.GetOwner()));
/// <summary>Gets the integrity level of the token of the owner that created or took ownership of the job.</summary>
/// <value>Integrity level of the token of the owner that created or took ownership of the job.</value>
[DefaultValue(8192)]
public uint OwnerIntegrityLevel => RunAction(() => IJob4.GetOwnerIntegrityLevel());
/// <summary>Gets a value that determines if the token of the owner was elevated at the time they created or took ownership of the job.</summary>
/// <value>Is TRUE if the token of the owner was elevated at the time they created or took ownership of the job; otherwise, FALSE.</value>
[DefaultValue(false)]
public bool OwnerIsElevated => RunAction(() => IJob4.GetOwnerElevationState());
/// <summary>
/// Gets or sets the priority level for the job. The priority level determines when the job is processed relative to other jobs in the transfer queue.
/// </summary>
[DefaultValue(BackgroundCopyJobPriority.Normal)]
public BackgroundCopyJobPriority Priority
{
get => RunAction(() => (BackgroundCopyJobPriority)m_ijob.GetPriority(), BackgroundCopyJobPriority.Normal);
set => RunAction(() => m_ijob.SetPriority((BG_JOB_PRIORITY)value));
}
/// <summary>Gets the job-related progress information, such as the number of bytes and files transferred.</summary>
public BackgroundCopyJobProgress Progress => RunAction(() => new BackgroundCopyJobProgress(m_ijob.GetProgress()), new BackgroundCopyJobProgress());
/// <summary>
/// Gets or sets the proxy information that the job uses to transfer the files. A <c>null</c> value represents the system default proxy settings.
/// </summary>
/// <exception cref="ArgumentException">The WebProxy.Credentials property value contains a value. Use the SetCredentials method instead.</exception>
[DefaultValue(null)]
public System.Net.WebProxy Proxy
{
get => RunAction(() =>
{
m_ijob.GetProxySettings(out var pUse, out var pList, out var byList);
if (pUse == BG_JOB_PROXY_USAGE.BG_JOB_PROXY_USAGE_OVERRIDE)
return new System.Net.WebProxy(pList.ToString()?.Split(' ').FirstOrDefault(), true, byList.ToString()?.Split(' '));
else if (pUse == BG_JOB_PROXY_USAGE.BG_JOB_PROXY_USAGE_NO_PROXY)
return new System.Net.WebProxy();
return null;
});
set => RunAction(() =>
{
if (value == null)
m_ijob.SetProxySettings(BG_JOB_PROXY_USAGE.BG_JOB_PROXY_USAGE_PRECONFIG, null, null);
else if (string.IsNullOrEmpty(value.Address.AbsoluteUri))
m_ijob.SetProxySettings(BG_JOB_PROXY_USAGE.BG_JOB_PROXY_USAGE_NO_PROXY, null, null);
else
m_ijob.SetProxySettings(BG_JOB_PROXY_USAGE.BG_JOB_PROXY_USAGE_OVERRIDE, value.Address.AbsoluteUri, string.Join(" ", value.BypassList));
if (value.Credentials != null)
throw new ArgumentException("The set Proxy property does not support proxy credentials. Please use the SetCredentials method.");
});
}
/// <summary>Gets an in-memory copy of the reply data from the server application.</summary>
public byte[] ReplyData
{
get => RunAction(() =>
{
IJob2.GetReplyData(out var pdata, out var cRet);
return (cRet > 0) ? pdata.ToArray<byte>((int)cRet) : new byte[0];
}, new byte[0]);
}
/// <summary>Gets or sets the name of the file that contains the reply data from the server application.</summary>
[DefaultValue(null)]
public string ReplyFileName
{
get => RunAction(() => IJob2.GetReplyFileName(), string.Empty);
set => RunAction(() => IJob2.SetReplyFileName(value));
}
/// <summary>Gets progress information related to the transfer of the reply data from an upload-reply job.</summary>
public BackgroundCopyJobReplyProgress ReplyProgress => RunAction(() => new BackgroundCopyJobReplyProgress(IJob2.GetReplyProgress()), new BackgroundCopyJobReplyProgress());
/// <summary>Gets the current state of the job.</summary>
public BackgroundCopyJobState State => RunAction(() => (BackgroundCopyJobState)m_ijob.GetState(), BackgroundCopyJobState.Error);
/// <summary>
/// Gets or sets the flags for HTTP that determine whether the certificate revocation list is checked and certain certificate errors are ignored, and the
/// policy to use when a server redirects the HTTP request.
/// </summary>
[DefaultValue(BackgroundCopyJobSecurity.AllowSilentRedirect)]
public BackgroundCopyJobSecurity SecurityOptions
{
get => RunAction(() => (BackgroundCopyJobSecurity)IHttpOp.GetSecurityFlags(), BackgroundCopyJobSecurity.AllowSilentRedirect);
set => RunAction(() => IHttpOp.SetSecurityFlags((BG_HTTP_SECURITY)value));
}
/// <summary>Used to control transfer behavior over cellular and/or similar networks. This property may be changed while a transfer is ongoing the new cost flags will take effect immediately.</summary>
[DefaultValue(BackgroundCopyCost.TransferUnrestricted)]
public BackgroundCopyCost TransferBehavior
{
get => (BackgroundCopyCost)GetProperty(BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_ID_COST_FLAGS);
set => SetProperty(BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_ID_COST_FLAGS, value);
}
/// <summary>Gets the time the job entered the Transferred state.</summary>
public DateTime TransferCompletionTime => Times.TransferCompletionTime.ToDateTime();
/// <summary>Marks a BITS job as being willing to include default credentials in requests to proxy servers. Enabling this property is equivalent to setting a WinHTTP security level of WINHTTP_AUTOLOGON_SECURITY_LEVEL_MEDIUM on the requests that BITS makes on the users behalf. The user BITS retrieves stored credentials from the is the same as the one it makes network requests on behalf of: BITS will normally use the job owners credentials, unless you have explicitly provided a network helper token, in which case BITS will use the network helper tokens credentials.
/// <para>Only the BG_AUTH_TARGET_PROXY target is supported.</para></summary>
[DefaultValue(BackgroundCopyJobCredentialTarget.Undefined)]
public BackgroundCopyJobCredentialTarget UseStoredCredentials
{
get => (BackgroundCopyJobCredentialTarget)(BG_AUTH_TARGET)GetProperty(BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_USE_STORED_CREDENTIALS);
set => SetProperty(BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_USE_STORED_CREDENTIALS, value);
}
private IBackgroundCopyJobHttpOptions IHttpOp => GetDerived<IBackgroundCopyJobHttpOptions>();
private IBackgroundCopyJob2 IJob2 => GetDerived<IBackgroundCopyJob2>();
private IBackgroundCopyJob3 IJob3 => GetDerived<IBackgroundCopyJob3>();
private IBackgroundCopyJob4 IJob4 => GetDerived<IBackgroundCopyJob4>();
private IBackgroundCopyJob5 IJob5 => GetDerived<IBackgroundCopyJob5>();
private BG_NOTIFY NotifyFlags
{
get => RunAction(() => m_ijob.GetNotifyFlags(), (BG_NOTIFY)0);
set => RunAction(() => {
BackgroundCopyJobState st = State;
if (st != BackgroundCopyJobState.Acknowledged && st != BackgroundCopyJobState.Cancelled)
m_ijob.SetNotifyFlags(value);
});
}
private BG_JOB_TIMES Times => RunAction(() => m_ijob.GetTimes(), new BG_JOB_TIMES());
/// <summary>
/// Use the Cancel method to delete the job from the transfer queue and to remove related temporary files from the client (downloads) and server
/// (uploads). You can cancel a job at any time; however, the job cannot be recovered after it is canceled.
/// </summary>
public void Cancel() => RunAction(() => m_ijob.Cancel());
/// <summary>
/// Use the RemoveCredentials method to remove credentials from use. The credentials must match an existing target and scheme pair that you specified
/// using the IBackgroundCopyJob2::SetCredentials method. There is no method to retrieve the credentials you have set.
/// </summary>
public void ClearCredentials()
{
try { IJob2.RemoveCredentials(BG_AUTH_TARGET.BG_AUTH_TARGET_SERVER, BG_AUTH_SCHEME.BG_AUTH_SCHEME_BASIC); }
catch (COMException) { }
try { IJob2.RemoveCredentials(BG_AUTH_TARGET.BG_AUTH_TARGET_SERVER, BG_AUTH_SCHEME.BG_AUTH_SCHEME_NTLM); }
catch (COMException) { }
}
/// <summary>Use the Complete method to end the job and save the transferred files on the client.</summary>
public void Complete() => RunAction(() => m_ijob.Complete());
/// <summary>Returns a hash code for this instance.</summary>
/// <returns>A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.</returns>
public override int GetHashCode() => ID.GetHashCode();
/// <summary>Use the ReplaceRemotePrefix method to replace the beginning text of all remote names in the download job with the given string.</summary>
/// <param name="oldPrefix">String that identifies the text to replace in the remote name. The text must start at the beginning of the remote name.</param>
/// <param name="newPrefix">String that contains the replacement text.</param>
public void ReplaceRemotePrefix(string oldPrefix, string newPrefix) => RunAction(() => IJob3.ReplaceRemotePrefix(oldPrefix, newPrefix));
/// <summary>Use the Resume method to activate a new job or restart a job that has been suspended.</summary>
public void Resume() => RunAction(() => m_ijob.Resume());
/// <summary>Specifies the identifier of the client certificate to use for client authentication in an HTTPS (SSL) request.</summary>
/// <param name="store">The certificate store.</param>
/// <param name="cert">The certificate.</param>
public void SetCertificate(X509Store store, X509Certificate2 cert)
{
var loc = BG_CERT_STORE_LOCATION.BG_CERT_STORE_LOCATION_CURRENT_USER;
switch (store.Location)
{
case StoreLocation.LocalMachine:
loc = BG_CERT_STORE_LOCATION.BG_CERT_STORE_LOCATION_LOCAL_MACHINE;
break;
case StoreLocation.CurrentUser:
default:
break;
}
IHttpOp.SetClientCertificateByID(loc, store.Name, cert.GetCertHash());
}
/// <summary>Use the SetCredentials method to specify the credentials to use for a proxy or remote server user authentication request.</summary>
/// <param name="cred">Identifies the user's credentials to use for user authentication.</param>
/// <param name="target">Identifies the target for these credentials.</param>
public void SetCredentials(System.Net.NetworkCredential cred, BackgroundCopyJobCredentialTarget target = BackgroundCopyJobCredentialTarget.Server)
{
var ac = new BG_AUTH_CREDENTIALS { Target = (BG_AUTH_TARGET)target };
if (string.IsNullOrEmpty(cred.Domain))
{
if (!string.IsNullOrEmpty(cred.UserName))
{
ac.Scheme = BG_AUTH_SCHEME.BG_AUTH_SCHEME_BASIC;
ac.Credentials.Basic.UserName = cred.UserName;
ac.Credentials.Basic.Password = cred.Password;
}
else
ac.Scheme = BG_AUTH_SCHEME.BG_AUTH_SCHEME_NTLM;
}
else
{
ac.Scheme = BG_AUTH_SCHEME.BG_AUTH_SCHEME_NTLM;
ac.Credentials.Basic.UserName = string.Concat(cred.Domain, '\\', cred.UserName);
ac.Credentials.Basic.Password = cred.Password;
}
RunAction(() => IJob2.SetCredentials(ref ac));
}
/// <summary>
/// Use the Suspend method to suspend a job. New jobs, jobs that are in error, and jobs that have finished transferring files are automatically suspended.
/// </summary>
public void Suspend() => RunAction(() => m_ijob.Suspend());
/// <summary>
/// Use the TakeOwnership method to change ownership of the job to the current user. To take ownership of the job, the user must have administrator
/// privileges on the client.
/// </summary>
public void TakeOwnership() => RunAction(() => m_ijob.TakeOwnership());
/// <summary>Returns a <see cref="System.String" /> that represents this instance.</summary>
/// <returns>A <see cref="System.String" /> that represents this instance.</returns>
public override string ToString() => string.Concat($"Job: {DisplayName}", string.IsNullOrEmpty(Description) ? "" : $" - {Description}", $" ({ID})");
/// <summary>Disposes of the BackgroundCopyJob object.</summary>
void IDisposable.Dispose()
{
try
{
NotifyFlags = 0;
m_ijob.SetNotifyInterface(null);
}
catch { }
Files = null;
Marshal.FinalReleaseComObject(m_ijob);
m_ijob = null;
m_notifier = null;
}
/// <summary>Called when the job has completed.</summary>
protected virtual void OnCompleted()
{
Completed?.Invoke(this, new BackgroundCopyJobEventArgs(this));
}
protected virtual void OnError(IBackgroundCopyError err)
{
Error?.Invoke(this, new BackgroundCopyJobEventArgs(this));
}
protected virtual void OnFileRangesTransferred(IBackgroundCopyFile file, BG_FILE_RANGE[] ranges)
{
FileRangesTransferred?.Invoke(this, new BackgroundCopyFileRangesTransferredEventArgs(this, file, ranges));
}
protected virtual void OnFileTransferred(IBackgroundCopyFile pFile)
{
FileTransferred?.Invoke(this, new BackgroundCopyFileTransferredEventArgs(this, pFile));
}
/// <summary>Called when the job has been modified.</summary>
protected virtual void OnModified()
{
Modified?.Invoke(this, new BackgroundCopyJobEventArgs(this));
}
private T GetDerived<T>() where T : class
{
T ret = m_ijob as T;
return ret ?? throw new PlatformNotSupportedException();
}
private object GetProperty(BITS_JOB_PROPERTY_ID id)
{
var value = RunAction(() => IJob5.GetProperty(id));
switch (id)
{
case BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_MAX_DOWNLOAD_SIZE:
return value.Uint64;
case BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_ID_COST_FLAGS:
case BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_MINIMUM_NOTIFICATION_INTERVAL_MS:
return value.Dword;
case BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_NOTIFICATION_CLSID:
return value.ClsID;
case BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_DYNAMIC_CONTENT:
case BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_HIGH_PERFORMANCE:
case BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_ON_DEMAND_MODE:
return value.Enable;
case BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_USE_STORED_CREDENTIALS:
return value.Target;
default:
throw new ArgumentOutOfRangeException(nameof(id));
}
}
private void HandleCOMException(COMException cex)
{
if (State == BackgroundCopyJobState.Error || State == BackgroundCopyJobState.TransientError)
{
OnError(m_ijob.GetError());
}
else
throw new BackgroundCopyException(cex);
}
private void RunAction(Action action)
{
try { action(); }
catch (COMException cex) { HandleCOMException(cex); }
}
private T RunAction<T>(Func<T> action, T def = default(T))
{
try { return action(); }
catch (COMException cex) { HandleCOMException(cex); }
return def;
}
private void SetProperty(BITS_JOB_PROPERTY_ID id, object value)
{
var str = new BITS_JOB_PROPERTY_VALUE();
switch (id)
{
case BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_MAX_DOWNLOAD_SIZE:
str.Uint64 = (ulong)value;
break;
case BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_ID_COST_FLAGS:
case BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_MINIMUM_NOTIFICATION_INTERVAL_MS:
case BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_USE_STORED_CREDENTIALS:
str.Dword = (uint)value;
break;
case BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_NOTIFICATION_CLSID:
str.ClsID = (Guid)value;
break;
case BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_DYNAMIC_CONTENT:
case BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_HIGH_PERFORMANCE:
case BITS_JOB_PROPERTY_ID.BITS_JOB_PROPERTY_ON_DEMAND_MODE:
str.Enable = (bool)value;
break;
default:
throw new ArgumentOutOfRangeException(nameof(id));
}
RunAction(() => IJob5.SetProperty(id, str));
}
[ComVisible(true)]
internal class Notifier : IBackgroundCopyCallback, IBackgroundCopyCallback2, IBackgroundCopyCallback3
{
private BackgroundCopyJob parent;
public Notifier(BackgroundCopyJob job)
{
parent = job;
}
private Notifier() { }
public void FileRangesTransferred(IBackgroundCopyJob job, IBackgroundCopyFile file, uint rangeCount, BG_FILE_RANGE[] ranges)
{
parent.OnFileRangesTransferred(file, ranges);
}
public void FileTransferred(IBackgroundCopyJob pJob, IBackgroundCopyFile pFile)
{
parent.OnFileTransferred(pFile);
}
public void JobError(IBackgroundCopyJob pJob, IBackgroundCopyError pError)
{
parent.OnError(pError);
}
public void JobModification(IBackgroundCopyJob pJob, uint dwReserved)
{
parent.OnModified();
}
public void JobTransferred(IBackgroundCopyJob pJob)
{
parent.OnCompleted();
}
}
}
/// <summary>Event argument for background copy job.</summary>
public class BackgroundCopyJobEventArgs : EventArgs
{
internal BackgroundCopyJobEventArgs(BackgroundCopyJob j)
{
Job = j;
}
/// <summary>Gets the job being processed.</summary>
/// <value>The job.</value>
public BackgroundCopyJob Job { get; private set; }
}
}

View File

@ -0,0 +1,197 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using static Vanara.PInvoke.BITS;
namespace Vanara.IO
{
/// <summary>Manages the set of jobs for the background copy service (BITS).</summary>
public class BackgroundCopyJobCollection : ICollection<BackgroundCopyJob>
{
internal BackgroundCopyJobCollection()
{
}
/// <summary>Gets the number of jobs currently managed by BITS.</summary>
public int Count
{
get
{
try
{
var ienum = BackgroundCopyManager.EnumJobs((BG_JOB_ENUM)JobListRights);
return (int)ienum.GetCount();
}
catch (COMException cex)
{
throw new BackgroundCopyException(cex);
}
}
}
/// <summary>Gets a value indicating whether the <see cref="ICollection{T}"/> is read-only.</summary>
bool ICollection<BackgroundCopyJob>.IsReadOnly => false;
/// <summary>Gets the correct flag for enumerating jobs based on whether user has administrator rights.</summary>
private uint JobListRights => BackgroundCopyManager.IsCurrentUserAdministrator() ? 1u : 0u;
/// <summary>Gets the <see cref="BackgroundCopyJob"/> object with the specified job identifier.</summary>
/// <param name="jobId">Unique identifier of the job.</param>
/// <returns>The referenced <see cref="BackgroundCopyJob"/> object if found, null if not.</returns>
public BackgroundCopyJob this[Guid jobId]
{
get
{
var job = BackgroundCopyManager.GetJob(jobId);
return job != null ? new BackgroundCopyJob(job) : throw new KeyNotFoundException();
}
}
/// <summary>Creates a new upload or download transfer job.</summary>
/// <param name="displayName">Name of the job.</param>
/// <param name="description">Description of the job.</param>
/// <param name="jobType">Type (upload or download) of the job.</param>
/// <returns>The new <see cref="BackgroundCopyJob"/>.</returns>
public BackgroundCopyJob Add(string displayName, string description = "", BackgroundCopyJobType jobType = BackgroundCopyJobType.Download)
{
try
{
var job = new BackgroundCopyJob(BackgroundCopyManager.CreateJob(displayName, (BG_JOB_TYPE)jobType));
if (!string.IsNullOrEmpty(description))
job.Description = description;
return job;
}
catch (COMException cex)
{
BackgroundCopyManager.HandleCOMException(cex);
}
return null;
}
/// <summary>Removes all items from the <see cref="ICollection{T}"/>.</summary>
public void Clear()
{
foreach (var i in this)
Remove(i);
}
/// <summary>Determines whether the <see cref="ICollection{T}"/> contains a specific value.</summary>
/// <param name="jobId">The object to locate in the <see cref="ICollection{T}"/>.</param>
/// <returns>true if <paramref name="jobId"/> is found in the <see cref="ICollection{T}"/>; otherwise, false.</returns>
public bool Contains(Guid jobId)
{
try
{
var ijob = BackgroundCopyManager.GetJob(jobId);
return ijob != null;
}
catch
{
return false;
}
}
/// <summary>Returns an enumerator that iterates through the collection.</summary>
/// <returns>A <see cref="IEnumerator{T}"/> that can be used to iterate through the collection.</returns>
public IEnumerator<BackgroundCopyJob> GetEnumerator()
{
var ienum = BackgroundCopyManager.EnumJobs((BG_JOB_ENUM)JobListRights);
return new Enumerator(ienum);
}
/// <summary> Removes the first occurrence of a specific object from the <see cref=""ICollection{T}" />. </summary> <param name="item">The object to
/// remove from the <see cref=""ICollection{T}" />.</param> <returns> true if <paramref name="item" /> was successfully removed from the <see
/// cref=""ICollection{T}" />; otherwise, false. This method also returns false if <paramref name="item" /> is not found in the original <see
/// cref=""ICollection{T}" />. </returns> <exception cref="ArgumentNullException">item</exception>
public bool Remove(BackgroundCopyJob item)
{
if (item == null) throw new ArgumentNullException(nameof(item));
// TODO: Look at what needs to be done to really remove a job and all it's actions
try { item.Cancel(); return true; } catch { return false; }
}
/// <summary>Returns a <see cref="System.String"/> that represents this instance.</summary>
/// <returns>A <see cref="System.String"/> that represents this instance.</returns>
public override string ToString() => $"Jobs: {Count}";
/// <summary>Adds an item to the <see cref=""ICollection{T}" />.</summary> <param name="item">The object to add to the <see cref=""ICollection{T}" />.</param>
void ICollection<BackgroundCopyJob>.Add(BackgroundCopyJob item)
{
}
/// <summary>Determines whether the <see cref="ICollection{T}"/> contains a specific value.</summary>
/// <param name="item">The object to locate in the <see cref="ICollection{T}"/>.</param>
/// <returns>true if <paramref name="item"/> is found in the <see cref="ICollection{T}"/>; otherwise, false.</returns>
/// <exception cref="NotImplementedException"></exception>
bool ICollection<BackgroundCopyJob>.Contains(BackgroundCopyJob item) => Contains(item.ID);
/// <summary> Copies the elements of the <see cref=""ICollection{T}" /> to an <see cref="Array" />, starting at a particular <see cref="Array" /> index.
/// </summary> <param name="array">The one-dimensional <see cref="Array" /> that is the destination of the elements copied from <see
/// cref=""ICollection{T}" />. The <see cref="Array" /> must have zero-based indexing.</param> <param name="arrayIndex">The zero-based index in <paramref
/// name="array" /> at which copying begins.</param> <exception cref="NotImplementedException"></exception>
void ICollection<BackgroundCopyJob>.CopyTo(BackgroundCopyJob[] array, int arrayIndex)
{
var ijobs = BackgroundCopyManager.EnumJobs((BG_JOB_ENUM)JobListRights);
var cnt = ijobs.GetCount();
Array.Copy(Array.ConvertAll(ijobs.Next(cnt), i => new BackgroundCopyJob(i)), 0, array, arrayIndex, cnt);
}
/// <summary>Returns an enumerator that iterates through a collection.</summary>
/// <returns>An <see cref="IEnumerator"/> object that can be used to iterate through the collection.</returns>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
/// <summary>
/// An implementation the <see cref="IEnumerator"/> interface that can iterate through the <see cref="BackgroundCopyJob"/> objects within the
/// <see cref="BackgroundCopyJobCollection"/> collection.
/// </summary>
private sealed class Enumerator : IEnumerator<BackgroundCopyJob>
{
private IBackgroundCopyJob icurrentjob;
private IEnumBackgroundCopyJobs ienum;
internal Enumerator(IEnumBackgroundCopyJobs enumjobs)
{
ienum = enumjobs;
ienum.Reset();
}
/// <summary>
/// Gets the <see cref="BackgroundCopyJob"/> object in the <see cref="BackgroundCopyJobCollection"/> collection to which the enumerator is pointing.
/// </summary>
public BackgroundCopyJob Current => icurrentjob != null ? new BackgroundCopyJob(icurrentjob) : throw new InvalidOperationException();
/// <summary>
/// Gets the <see cref="BackgroundCopyJob"/> object in the <see cref="BackgroundCopyJobCollection"/> collection to which the enumerator is pointing.
/// </summary>
object IEnumerator.Current => Current;
/// <summary>Disposes of the Enumerator object.</summary>
public void Dispose()
{
ienum = null;
icurrentjob = null;
}
/// <summary>Moves the enumerator index to the next object in the collection.</summary>
/// <returns></returns>
public bool MoveNext()
{
try
{
icurrentjob = ienum.Next(1)?.FirstOrDefault();
return icurrentjob != null;
}
catch { return false; }
}
/// <summary>Resets the enumerator index to the beginning of the <see cref="BackgroundCopyJobCollection"/> collection.</summary>
public void Reset()
{
icurrentjob = null;
ienum.Reset();
}
}
}
}

View File

@ -0,0 +1,172 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using static Vanara.PInvoke.BITS;
namespace Vanara.IO
{
/// <summary>Represents a single BITS job credential.</summary>
/// <seealso cref="System.IComparable{Vanara.IO.BackgroundCopyJobCredential}"/>
public class BackgroundCopyJobCredential : IComparable<BackgroundCopyJobCredential>
{
/// <summary>Initializes a new instance of the <see cref="BackgroundCopyJobCredential"/> class.</summary>
/// <param name="scheme">The scheme.</param>
/// <param name="target">The target.</param>
/// <param name="username">The user name.</param>
/// <param name="password">The password.</param>
public BackgroundCopyJobCredential(BackgroundCopyJobCredentialScheme scheme, BackgroundCopyJobCredentialTarget target, string username, string password)
{
Scheme = scheme;
Target = target;
UserName = username;
Password = password;
}
/// <summary>Gets the password.</summary>
/// <value>The password.</value>
public string Password { get; }
/// <summary>Gets the scheme.</summary>
/// <value>The scheme.</value>
public BackgroundCopyJobCredentialScheme Scheme { get; }
/// <summary>Gets the target.</summary>
/// <value>The target.</value>
public BackgroundCopyJobCredentialTarget Target { get; }
/// <summary>Gets the name of the user.</summary>
/// <value>The name of the user.</value>
public string UserName { get; }
internal uint Key => BackgroundCopyJobCredentials.MakeKey(Scheme, Target);
int IComparable<BackgroundCopyJobCredential>.CompareTo(BackgroundCopyJobCredential other) => Comparer<uint>.Default.Compare(Key, other.Key);
internal BG_AUTH_CREDENTIALS GetNative()
{
var cr = new BG_AUTH_CREDENTIALS { Scheme = (BG_AUTH_SCHEME)Scheme, Target = (BG_AUTH_TARGET)Target };
cr.Credentials.Basic.UserName = UserName;
cr.Credentials.Basic.Password = Password;
return cr;
}
}
/// <summary>The list of credentials for a job.</summary>
/// <seealso cref="System.IDisposable"/>
/// <seealso cref="System.Collections.Generic.ICollection{Vanara.IO.BackgroundCopyJobCredential}"/>
public class BackgroundCopyJobCredentials : IDisposable, ICollection<BackgroundCopyJobCredential>
{
private Dictionary<uint, BackgroundCopyJobCredential> dict;
private IBackgroundCopyJob2 ijob2;
internal BackgroundCopyJobCredentials(IBackgroundCopyJob2 job)
{
ijob2 = job;
}
/// <summary>Gets the number of elements contained in the <see cref="ICollection{T}"/>.</summary>
public int Count => dict?.Count ?? 0;
/// <summary>Gets a value indicating whether the <see cref="ICollection{T}"/> is read-only.</summary>
bool ICollection<BackgroundCopyJobCredential>.IsReadOnly => false;
private Dictionary<uint, BackgroundCopyJobCredential> Values => dict ?? (dict = new Dictionary<uint, BackgroundCopyJobCredential>());
/// <summary>Gets the <see cref="BackgroundCopyJobCredential"/> with the specified scheme and target.</summary>
/// <param name="scheme">The credential scheme.</param>
/// <param name="target">The credential target.</param>
/// <returns>The <see cref="BackgroundCopyJobCredential"/>.</returns>
public BackgroundCopyJobCredential this[BackgroundCopyJobCredentialScheme scheme, BackgroundCopyJobCredentialTarget target] =>
Values[MakeKey(scheme, target)];
/// <summary>Adds the specified credential.</summary>
/// <param name="cred">The credential.</param>
public void Add(BackgroundCopyJobCredential cred)
{
var cr = cred.GetNative();
ijob2.SetCredentials(ref cr);
Values.Add(cred.Key, cred);
}
/// <summary>Adds the specified credential.</summary>
/// <param name="scheme">The credential scheme.</param>
/// <param name="target">The credential target.</param>
/// <param name="username">The username.</param>
/// <param name="password">The password.</param>
public void Add(BackgroundCopyJobCredentialScheme scheme, BackgroundCopyJobCredentialTarget target, string username, string password)
{
Add(new BackgroundCopyJobCredential(scheme, target, username, password));
}
/// <summary>Removes all items from the <see cref="ICollection{T}"/>.</summary>
public void Clear()
{
if (dict == null) return;
foreach (var key in Values.Keys)
Remove(Values[key]);
}
/// <summary>Determines whether the <see cref="ICollection{T}"/> contains a specific value.</summary>
/// <param name="item">The object to locate in the <see cref="ICollection{T}"/>.</param>
/// <returns>true if <paramref name="item"/> is found in the <see cref="ICollection{T}"/>; otherwise, false.</returns>
public bool Contains(BackgroundCopyJobCredential item) => dict == null ? false : Values.ContainsKey(item.Key);
/// <summary>
/// Copies the elements of the <see cref="ICollection{T}"/> to an <see cref="T:System.Array"/>, starting at a particular <see cref="T:System.Array"/> index.
/// </summary>
/// <param name="array">
/// The one-dimensional <see cref="T:System.Array"/> that is the destination of the elements copied from <see cref="ICollection{T}"/>. The
/// <see cref="T:System.Array"/> must have zero-based indexing.
/// </param>
/// <param name="arrayIndex">The zero-based index in <paramref name="array"/> at which copying begins.</param>
public void CopyTo(BackgroundCopyJobCredential[] array, int arrayIndex)
{
if (dict == null) return;
Array.Copy(Values.Values.ToArray(), 0, array, arrayIndex, Count);
}
/// <summary>Returns an enumerator that iterates through the collection.</summary>
/// <returns>A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.</returns>
public IEnumerator<BackgroundCopyJobCredential> GetEnumerator() => Values.Values.GetEnumerator();
/// <summary>Removes the specified scheme.</summary>
/// <param name="scheme">The scheme.</param>
/// <param name="target">The target.</param>
/// <returns></returns>
public bool Remove(BackgroundCopyJobCredentialScheme scheme, BackgroundCopyJobCredentialTarget target)
{
try
{
ijob2.RemoveCredentials((BG_AUTH_TARGET)target, (BG_AUTH_SCHEME)scheme);
if (dict != null)
Values.Remove(MakeKey(scheme, target));
return true;
}
catch
{
return false;
}
}
/// <summary>Removes the first occurrence of a specific object from the <see cref="ICollection{T}"/>.</summary>
/// <param name="item">The object to remove from the <see cref="ICollection{T}"/>.</param>
/// <returns>
/// true if <paramref name="item"/> was successfully removed from the <see cref="ICollection{T}"/>; otherwise, false. This method also returns false if
/// <paramref name="item"/> is not found in the original <see cref="ICollection{T}"/>.
/// </returns>
public bool Remove(BackgroundCopyJobCredential item) => Remove(item.Scheme, item.Target);
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
void IDisposable.Dispose()
{
ijob2 = null;
}
/// <summary>Returns an enumerator that iterates through a collection.</summary>
/// <returns>An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.</returns>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
internal static uint MakeKey(BackgroundCopyJobCredentialScheme scheme, BackgroundCopyJobCredentialTarget target) => PInvoke.Macros.MAKELONG((ushort)scheme, (ushort)target);
}
}

View File

@ -0,0 +1,184 @@
using System;
using System.Runtime.InteropServices;
using System.Security.Principal;
#if !(NET20 || NET35 || NET40)
using System.Threading;
using System.Threading.Tasks;
#endif
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 Version ver;
/// <summary>Initializes a new instance of the <see cref="BackgroundCopyManager"/> class.</summary>
static BackgroundCopyManager()
{
IMgr = new IBackgroundCopyManager();
Jobs = new BackgroundCopyJobCollection();
}
/// <summary>Gets the list of currently queued jobs for all users.</summary>
public static BackgroundCopyJobCollection Jobs { get; private set; }
/// <summary>Retrieves the running version of BITS.</summary>
public static Version Version
{
get
{
try { return ver ?? (ver = GetVer()); }
catch { return new Version(); }
Version GetVer()
{
var fi = System.Diagnostics.FileVersionInfo.GetVersionInfo(Environment.ExpandEnvironmentVariables(@"%WinDir%\Sysnative\qmgr.dll"));
switch ($"{fi.FileMajorPart}.{fi.FileMinorPart}")
{
case "7.8": return new Version(10, 1);
case "7.7": return new Version(5, 0);
case "7.5": return new Version(4, 0);
case "7.0": return new Version(3, 0);
case "6.7": return new Version(2, 5);
case "6.6": return new Version(2, 0);
case "6.5": return new Version(1, 5);
case "6.2": return new Version(1, 2);
default: return new Version(1, 0);
}
}
}
}
/// <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(sourceFileName, destFileName, () => false, System.Threading.Thread.Sleep, null, f => f.Add(sourceFileName, destFileName));
}
#if !(NET20 || NET35 || NET40)
/// <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>
public static async Task CopyAsync(string sourceFileName, string destFileName, CancellationToken cancellationToken, IProgress<Tuple<BackgroundCopyJobState, byte>> progress)
{
await Task.Run(() => CopyTemplate(sourceFileName, destFileName, () => cancellationToken.IsCancellationRequested,
Thread.Sleep, (s, p) => progress?.Report(new Tuple<BackgroundCopyJobState, byte>(s,p)), f => f.Add(sourceFileName, destFileName)), cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
}
#endif
private static IBackgroundCopyManager IMgr { get; set; }
internal static IBackgroundCopyJob CreateJob(string displayName, BG_JOB_TYPE jobType = BG_JOB_TYPE.BG_JOB_TYPE_DOWNLOAD)
{
try
{
IMgr.CreateJob(displayName, jobType, out var newJobID, out var newJob);
return newJob;
}
catch (COMException cex)
{
HandleCOMException(cex);
}
return null;
}
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;
}
internal static void HandleCOMException(COMException cex) => throw new BackgroundCopyException(cex);
/// <summary>Checks if the current user has administrator rights.</summary>
internal static bool IsCurrentUserAdministrator()
{
var wp = new WindowsPrincipal(WindowsIdentity.GetCurrent());
return wp.IsInRole(WindowsBuiltInRole.Administrator);
}
private static void CopyTemplate(string sourceFileName, string destFileName, Func<bool> shouldCancel, Action<int> delay, Action<BackgroundCopyJobState, byte> report, Action<BackgroundCopyFileCollection> add)
{
var type = (Uri.TryCreate(destFileName, UriKind.Absolute, out var uri) && !uri.IsFile) ? BackgroundCopyJobType.Upload : BackgroundCopyJobType.Download;
using (var job = Jobs.Add("Temp" + Guid.NewGuid().ToString(), "", type))
{
job.DisableNotifications = true;
add(job.Files);
BackgroundCopyJobState state = BackgroundCopyJobState.Connecting;
job.Resume();
do
{
switch (state = job.State)
{
case BackgroundCopyJobState.Queued:
case BackgroundCopyJobState.Connecting:
case BackgroundCopyJobState.Transferring:
case BackgroundCopyJobState.Suspended:
ReportProgress();
break;
case BackgroundCopyJobState.Error:
case BackgroundCopyJobState.TransientError:
throw job.LastError;
case BackgroundCopyJobState.Transferred:
ReportProgress();
job.Complete();
return;
case BackgroundCopyJobState.Acknowledged:
case BackgroundCopyJobState.Cancelled:
return;
default:
throw new InvalidOperationException("Unknown job state");
}
if (shouldCancel())
{
job.Cancel();
break;
}
delay(1000);
void ReportProgress()
{
report?.Invoke(state, job.Progress.PercentComplete);
}
} while (state != BackgroundCopyJobState.Transferred && state != BackgroundCopyJobState.Error && state != BackgroundCopyJobState.TransientError);
}
}
}
}

214
System/BITS/Enums.cs Normal file
View File

@ -0,0 +1,214 @@
using System;
using static Vanara.PInvoke.BITS;
namespace Vanara.IO
{
/// <summary>Flags for ACL information to maintain when using SMB to download or upload a file.</summary>
[Flags]
public enum BackgroundCopyACLFlags
{
/// <summary>If set exclusively, BITS uses the default ACL information of the destination folder.</summary>
None = BG_COPY_FILE.BG_COPY_FILE_NONE,
/// <summary>
/// If set, the file's owner information is maintained. Otherwise, the user who calls the <see cref="BackgroundCopyJob.Complete"/> method owns the file.
/// </summary>
Owner = BG_COPY_FILE.BG_COPY_FILE_OWNER,
/// <summary>
/// If set, the file's group information is maintained. Otherwise, BITS uses the job owner's primary group to assign the group information to the file.
/// </summary>
Group = BG_COPY_FILE.BG_COPY_FILE_GROUP,
/// <summary>
/// If set, BITS copies the explicit ACEs from the source file and inheritable ACEs from the destination folder. Otherwise, BITS copies the inheritable
/// ACEs from the destination folder. If the destination folder does not contain inheritable ACEs, BITS uses the default DACL from the owner's account.
/// </summary>
Dacl = BG_COPY_FILE.BG_COPY_FILE_DACL,
/// <summary>
/// If set, BITS copies the explicit ACEs from the source file and inheritable ACEs from the destination folder. Otherwise, BITS copies the inheritable
/// ACEs from the destination folder.
/// </summary>
Sacl = BG_COPY_FILE.BG_COPY_FILE_SACL,
/// <summary>If set, BITS copies the owner and ACL information. This is the same as setting all the flags individually.</summary>
All = BG_COPY_FILE.BG_COPY_FILE_ALL
}
/// <summary>Defines the constant values that specify the BITS cost state.</summary>
[Flags]
public enum BackgroundCopyCost : uint
{
Unrestricted = BITS_COST_STATE.BITS_COST_STATE_UNRESTRICTED,
CappedUsageUnknown = BITS_COST_STATE.BITS_COST_STATE_CAPPED_USAGE_UNKNOWN,
BelowCap = BITS_COST_STATE.BITS_COST_STATE_BELOW_CAP,
NearCap = BITS_COST_STATE.BITS_COST_STATE_NEAR_CAP,
OvercapCharged = BITS_COST_STATE.BITS_COST_STATE_OVERCAP_CHARGED,
OstStateOvercapThrottled = BITS_COST_STATE.BITS_COST_STATE_OVERCAP_THROTTLED,
OstStateUsageBased = BITS_COST_STATE.BITS_COST_STATE_USAGE_BASED,
Roaming = BITS_COST_STATE.BITS_COST_STATE_ROAMING,
IgnoreCongestion = BITS_COST_STATE.BITS_COST_OPTION_IGNORE_CONGESTION,
Reserved = BITS_COST_STATE.BITS_COST_STATE_RESERVED,
TransferNotRoaming = BITS_COST_STATE.BITS_COST_STATE_TRANSFER_NOT_ROAMING,
TransferNoSurcharge = BITS_COST_STATE.BITS_COST_STATE_TRANSFER_NO_SURCHARGE,
TransferStandard = BITS_COST_STATE.BITS_COST_STATE_TRANSFER_STANDARD,
TransferUnrestricted = BITS_COST_STATE.BITS_COST_STATE_TRANSFER_UNRESTRICTED,
TransferAlways = BITS_COST_STATE.BITS_COST_STATE_TRANSFER_ALWAYS,
}
/// <summary>Defines the constant values that specify the context in which the error occurred.</summary>
public enum BackgroundCopyErrorContext
{
/// <summary>An error has not occurred.</summary>
None = BG_ERROR_CONTEXT.BG_ERROR_CONTEXT_NONE,
/// <summary>The error context is unknown.</summary>
Unknown = BG_ERROR_CONTEXT.BG_ERROR_CONTEXT_UNKNOWN,
/// <summary>The transfer queue manager generated the error.</summary>
GeneralQueueManager = BG_ERROR_CONTEXT.BG_ERROR_CONTEXT_GENERAL_QUEUE_MANAGER,
/// <summary>The error was generated while the queue manager was notifying the client of an event.</summary>
QueueManagerNotification = BG_ERROR_CONTEXT.BG_ERROR_CONTEXT_QUEUE_MANAGER_NOTIFICATION,
/// <summary>The error was related to the specified local file. For example, permission was denied or the volume was unavailable.</summary>
LocalFile = BG_ERROR_CONTEXT.BG_ERROR_CONTEXT_LOCAL_FILE,
/// <summary>The error was related to the specified remote file. For example, the URL was not accessible.</summary>
RemoteFile = BG_ERROR_CONTEXT.BG_ERROR_CONTEXT_REMOTE_FILE,
/// <summary>The transport layer generated the error. These errors are general transport failures (these errors are not specific to the remote file).</summary>
GeneralTransport = BG_ERROR_CONTEXT.BG_ERROR_CONTEXT_GENERAL_TRANSPORT,
/// <summary>The server application that BITS passed the upload file to generated an error while processing the upload file.</summary>
RemoteApplication = BG_ERROR_CONTEXT.BG_ERROR_CONTEXT_REMOTE_APPLICATION
}
/// <summary>Defines the constant values that specify the authentication scheme to use when a proxy or server requests user authentication.</summary>
public enum BackgroundCopyJobCredentialScheme
{
/// <summary>Basic is a scheme in which the user name and password are sent in clear-text to the server or proxy.</summary>
Basic = BG_AUTH_SCHEME.BG_AUTH_SCHEME_BASIC,
/// <summary>Digest is a challenge-response scheme that uses a server-specified data string for the challenge.</summary>
Digest = BG_AUTH_SCHEME.BG_AUTH_SCHEME_DIGEST,
/// <summary>
/// Simple and Protected Negotiation protocol (Snego) is a challenge-response scheme that negotiates with the server or proxy to determine which
/// scheme to use for authentication. Examples are the Kerberos protocol and NTLM.
/// </summary>
Negotiate = BG_AUTH_SCHEME.BG_AUTH_SCHEME_NEGOTIATE,
/// <summary>NTLM is a challenge-response scheme that uses the credentials of the user for authentication in a Windows network environment.</summary>
NTLM = BG_AUTH_SCHEME.BG_AUTH_SCHEME_NTLM,
/// <summary>Passport is a centralized authentication service provided by Microsoft that offers a single logon for member sites.</summary>
Passport = BG_AUTH_SCHEME.BG_AUTH_SCHEME_PASSPORT
}
/// <summary>HTTP security flags that indicate which errors to ignore when connecting to the server.</summary>
[Flags]
public enum BackgroundCopyJobSecurity
{
/// <summary>Allows the server to redirect your request to another server. This is the default.</summary>
AllowSilentRedirect = BG_HTTP_SECURITY.BG_HTTP_REDIRECT_POLICY_ALLOW_SILENT,
/// <summary>Check the certificate revocation list (CRL) to verify that the server certificate has not been revoked.</summary>
CheckCRL = BG_HTTP_SECURITY.BG_SSL_ENABLE_CRL_CHECK,
/// <summary>Ignores errors caused when the certificate host name of the server does not match the host name in the request.</summary>
IgnoreInvalidCerts = BG_HTTP_SECURITY.BG_SSL_IGNORE_CERT_CN_INVALID,
/// <summary>Ignores errors caused by an expired certificate.</summary>
IgnoreExpiredCerts = BG_HTTP_SECURITY.BG_SSL_IGNORE_CERT_DATE_INVALID,
/// <summary>Ignore errors associated with an unknown certification authority (CA).</summary>
IgnoreUnknownCA = BG_HTTP_SECURITY.BG_SSL_IGNORE_UNKNOWN_CA,
/// <summary>Ignore errors associated with the use of a certificate.</summary>
IgnoreWrongCertUsage = BG_HTTP_SECURITY.BG_SSL_IGNORE_CERT_WRONG_USAGE,
/// <summary>Allows the server to redirect your request to another server. BITS updates the remote name with the final URL.</summary>
AllowReportedRedirect = BG_HTTP_SECURITY.BG_HTTP_REDIRECT_POLICY_ALLOW_REPORT,
/// <summary>
/// Places the job in the fatal error state when the server redirects your request to another server. BITS updates the remote name with the redirected URL.
/// </summary>
DisallowRedirect = BG_HTTP_SECURITY.BG_HTTP_REDIRECT_POLICY_DISALLOW,
/// <summary>
/// Allows the server to redirect an HTTPS request to an HTTP URL.
/// <para>You can combine this flag with AllowSilentRedirect and AllowReportedRedirect.</para>
/// </summary>
AllowHttpsToHttpRedirect = BG_HTTP_SECURITY.BG_HTTP_REDIRECT_POLICY_ALLOW_HTTPS_TO_HTTP,
}
/// <summary>Defines the constant values that specify whether the credentials are used for proxy or server user authentication requests.</summary>
public enum BackgroundCopyJobCredentialTarget : uint
{
/// <summary>Undefined.</summary>
Undefined = 0,
/// <summary>Use credentials for server requests.</summary>
Server = BG_AUTH_TARGET.BG_AUTH_TARGET_SERVER,
/// <summary>Use credentials for proxy requests.</summary>
Proxy = BG_AUTH_TARGET.BG_AUTH_TARGET_PROXY
}
/// <summary>Defines the constant values that specify the priority level of a job.</summary>
public enum BackgroundCopyJobPriority
{
/// <summary>
/// Transfers the job in the foreground. Foreground transfers compete for network bandwidth with other applications, which can impede the user's network
/// experience. This is the highest priority level.
/// </summary>
Foreground = BG_JOB_PRIORITY.BG_JOB_PRIORITY_FOREGROUND,
/// <summary>
/// Transfers the job in the background with a high priority. Background transfers use idle network bandwidth of the client to transfer files. This is
/// the highest background priority level.
/// </summary>
High = BG_JOB_PRIORITY.BG_JOB_PRIORITY_HIGH,
/// <summary>
/// Transfers the job in the background with a normal priority. Background transfers use idle network bandwidth of the client to transfer files. This is
/// the default priority level.
/// </summary>
Normal = BG_JOB_PRIORITY.BG_JOB_PRIORITY_NORMAL,
/// <summary>
/// Transfers the job in the background with a low priority. Background transfers use idle network bandwidth of the client to transfer files. This is the
/// lowest background priority level.
/// </summary>
Low = BG_JOB_PRIORITY.BG_JOB_PRIORITY_LOW
}
/// <summary>Defines constant values for the different states of a job.</summary>
public enum BackgroundCopyJobState
{
/// <summary>
/// Specifies that the job is in the queue and waiting to run. If a user logs off while their job is transferring, the job transitions to the queued state.
/// </summary>
Queued = BG_JOB_STATE.BG_JOB_STATE_QUEUED,
/// <summary>
/// Specifies that BITS is trying to connect to the server. If the connection succeeds, the state of the job becomes Transferring; otherwise, the state
/// becomes TransientError.
/// </summary>
Connecting = BG_JOB_STATE.BG_JOB_STATE_CONNECTING,
/// <summary>Specifies that BITS is transferring data for the job.</summary>
Transferring = BG_JOB_STATE.BG_JOB_STATE_TRANSFERRING,
/// <summary>
/// Specifies that the job is suspended (paused). To suspend a job, call the <see cref="BackgroundCopyJob.Suspend"/> method. BITS automatically suspends
/// a job when it is created. The job remains suspended until you call the <see cref="BackgroundCopyJob.Resume"/>,
/// <see cref="BackgroundCopyJob.Complete"/>, or <see cref="BackgroundCopyJob.Cancel"/> method.
/// </summary>
Suspended = BG_JOB_STATE.BG_JOB_STATE_SUSPENDED,
/// <summary>
/// Specifies that a non-recoverable error occurred (the service is unable to transfer the file). If the error, such as an access-denied error, can be
/// corrected, call the <see cref="BackgroundCopyJob.Resume"/> method after the error is fixed. However, if the error cannot be corrected, call the
/// <see cref="BackgroundCopyJob.Cancel"/> method to cancel the job, or call the <see cref="BackgroundCopyJob.Complete"/> method to accept the portion of
/// a download job that transferred successfully.
/// </summary>
Error = BG_JOB_STATE.BG_JOB_STATE_ERROR,
/// <summary>
/// Specifies that a recoverable error occurred. BITS will retry jobs in the transient error state based on the retry interval you specify (see
/// <see cref="BackgroundCopyJob.MinimumRetryDelay"/>). The state of the job changes to <see cref="BackgroundCopyJobState.Error"/> if the job fails to
/// make progress (see <see cref="BackgroundCopyJob.NoProgressTimeout"/>). BITS does not retry the job if a network disconnect or disk lock error
/// occurred (for example, chkdsk is running) or the MaxInternetBandwidth Group Policy is zero.
/// </summary>
TransientError = BG_JOB_STATE.BG_JOB_STATE_TRANSIENT_ERROR,
/// <summary>
/// Specifies that your job was successfully processed. You must call the <see cref="BackgroundCopyJob.Complete"/> method to acknowledge completion of
/// the job and to make the files available to the client.
/// </summary>
Transferred = BG_JOB_STATE.BG_JOB_STATE_TRANSFERRED,
/// <summary>Specifies that you called the <see cref="BackgroundCopyJob.Complete"/> method to acknowledge that your job completed successfully.</summary>
Acknowledged = BG_JOB_STATE.BG_JOB_STATE_ACKNOWLEDGED,
/// <summary>Specifies that you called the <see cref="BackgroundCopyJob.Cancel"/> method to cancel the job (remove the job from the transfer queue).</summary>
Cancelled = BG_JOB_STATE.BG_JOB_STATE_CANCELLED
}
/// <summary>Defines constant values that specify the type of transfer job, such as download.</summary>
public enum BackgroundCopyJobType
{
/// <summary>Specifies that the job downloads files to the client.</summary>
Download = BG_JOB_TYPE.BG_JOB_TYPE_DOWNLOAD,
/// <summary>Specifies that the job uploads a file to the server.</summary>
Upload = BG_JOB_TYPE.BG_JOB_TYPE_UPLOAD,
/// <summary>Specifies that the job uploads a file to the server and receives a reply file from the server application.</summary>
UploadReply = BG_JOB_TYPE.BG_JOB_TYPE_UPLOAD_REPLY
}
}

View File

@ -58,6 +58,7 @@ DeviceType, ProcessIntegrityLevel, Subtype
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="4.4.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PInvoke\BITS\Vanara.PInvoke.BITS.csproj" />
<ProjectReference Include="..\PInvoke\Kernel32\Vanara.PInvoke.Kernel32.csproj" />
<ProjectReference Include="..\PInvoke\NetListMgr\Vanara.PInvoke.NetListMgr.csproj" />
<ProjectReference Include="..\PInvoke\User32\Vanara.PInvoke.User32.csproj" />

43
System/Wow64Redirect.cs Normal file
View File

@ -0,0 +1,43 @@
using System;
using static Vanara.PInvoke.Kernel32;
namespace Vanara.IO
{
/// <summary>
/// Suspends File System Redirection if found to be in effect. Effectively, this calls <c>IsWow64Process</c> to determine state and then disables
/// redirection using <c>Wow64DisableWow64FsRedirection</c>. It then reverts redirection at disposal using <c>Wow64RevertWow64FsRedirection</c>.
/// </summary>
/// <remarks>
/// This class is best used in a <c>using</c> clause as follows:
/// <code lang="cs">
/// using (new Wow64Redirect())
/// {
/// if (System.IO.File.Exists(@"C:\Windows\System32\qmgr.dll"))
/// {
/// // Do something
/// }
/// }
/// </code>
/// </remarks>
public class Wow64Redirect : IDisposable
{
private readonly bool isWow64;
private readonly IntPtr oldVal;
/// <summary>Initializes a new instance of the <see cref="Wow64Redirect"/> class.</summary>
public Wow64Redirect()
{
if (isWow64 = (IsWow64Process(GetCurrentProcess(), out var wow) && wow))
Wow64DisableWow64FsRedirection(out oldVal);
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
void IDisposable.Dispose()
{
if (isWow64)
Wow64RevertWow64FsRedirection(oldVal);
}
}
}

View File

@ -0,0 +1,250 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
namespace Vanara.IO.Tests
{
[TestFixture()]
public class BackgroundCopyTests
{
const string jname = "TestJob";
const string src = @"file:///C:/Temp/Holes.mp4";
const string dest = @"D:\dest.bin";
[Test]
public void EnumJobTest()
{
var cnt = 0;
Assert.That(() => BackgroundCopyManager.Jobs.Count, Throws.Nothing);
Assert.That(cnt = BackgroundCopyManager.Jobs.Count, Is.GreaterThanOrEqualTo(0));
Assert.That(BackgroundCopyManager.Jobs.Count(), Is.EqualTo(cnt));
}
[Test]
public void VerTest()
{
Assert.That(BackgroundCopyManager.Version, Is.GreaterThanOrEqualTo(new Version(10, 0)));
}
[Test]
public void JobCollTest()
{
var guid = Guid.Empty;
BackgroundCopyJob job = null;
Assert.That(() => { var j = BackgroundCopyManager.Jobs.Add(jname); guid = j.ID; }, Throws.Nothing);
Assert.That(BackgroundCopyManager.Jobs.Count, Is.GreaterThanOrEqualTo(1));
Assert.That(BackgroundCopyManager.Jobs.Contains(guid), Is.True);
Assert.That(() => job = BackgroundCopyManager.Jobs[guid], Throws.Nothing);
Assert.That(job, Is.Not.Null);
Assert.That(BackgroundCopyManager.Jobs.Count(j => j.ID == guid), Is.EqualTo(1));
var array = new BackgroundCopyJob[BackgroundCopyManager.Jobs.Count];
Assert.That(() => ((ICollection<BackgroundCopyJob>)BackgroundCopyManager.Jobs).CopyTo(array, 0), Throws.Nothing);
Assert.That(array[0], Is.Not.Null);
Assert.That(() => BackgroundCopyManager.Jobs.Remove(job), Throws.Nothing);
Assert.That(BackgroundCopyManager.Jobs.Contains(guid), Is.False);
}
[Test]
public void FileCollTest()
{
var job = BackgroundCopyManager.Jobs.Add(jname);
System.IO.File.Delete(dest);
Assert.That(() => job.Files.Add(src, dest), Throws.Nothing);
Assert.That(job.Files.Count, Is.EqualTo(1));
Assert.That(job.Files.Count(), Is.EqualTo(1));
Assert.That(job.Files.First().LocalFilePath, Is.EqualTo(dest));
Assert.That(() => job.Cancel(), Throws.Nothing);
}
[Test]
public void CopyTest()
{
System.IO.File.Delete(dest);
Assert.That(() => BackgroundCopyManager.Copy(src, dest), Throws.Nothing);
Assert.That(System.IO.File.Exists(dest));
System.IO.File.Delete(dest);
}
[Test]
public void CopyAsyncCancelReportTest()
{
System.IO.File.Delete(dest);
var cts = new CancellationTokenSource();
var l = new List<string>();
var prog = new Progress<Tuple<BackgroundCopyJobState, byte>>(t => l.Add($"{t.Item2}% : {t.Item1}"));
cts.CancelAfter(2000);
Assert.That(() => BackgroundCopyManager.CopyAsync(src, dest, cts.Token, prog), Throws.TypeOf<TaskCanceledException>());
Assert.That(System.IO.File.Exists(dest), Is.False);
Assert.That(l.Count, Is.Zero);
TestContext.Write(string.Join("\r\n", l));
}
[Test]
public void CopyAsyncReportTest()
{
System.IO.File.Delete(dest);
var cts = new CancellationTokenSource();
var l = new List<string>();
var prog = new Progress<Tuple<BackgroundCopyJobState, byte>>(t => l.Add($"{t.Item2}% : {t.Item1}"));
Assert.That(() => BackgroundCopyManager.CopyAsync(src, dest, cts.Token, prog), Throws.Nothing);
Assert.That(System.IO.File.Exists(dest), Is.True);
Assert.That(l.Count, Is.GreaterThan(0));
TestContext.Write(string.Join("\r\n", l));
}
[Test]
public void CopyAsyncTest()
{
System.IO.File.Delete(dest);
var cts = new CancellationTokenSource();
Assert.That(() => BackgroundCopyManager.CopyAsync(src, dest, cts.Token, null), Throws.Nothing);
Assert.That(System.IO.File.Exists(dest), Is.True);
System.IO.File.Delete(dest);
}
[Test]
public void JobCertTest()
{
var job = BackgroundCopyManager.Jobs.Add(jname);
Assert.That(job.Certificate, Is.Null);
var store = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
var c = store.Certificates.Cast<X509Certificate2>().FirstOrDefault();
job.SetCertificate(store, c);
Assert.That(job.Certificate, Is.EqualTo(c));
store.Close();
}
[Test]
public void JobPropTest()
{
var job = BackgroundCopyManager.Jobs.Add(jname);
Assert.That(job.DisplayName, Is.EqualTo(jname));
Assert.That(() => job.ACLFlags = BackgroundCopyACLFlags.All, Throws.Nothing);
Assert.That(job.ACLFlags, Is.EqualTo(BackgroundCopyACLFlags.All));
Assert.That(job.Credentials.Count, Is.EqualTo(0));
Assert.That(() => job.Credentials.Add(BackgroundCopyJobCredentialScheme.Digest, BackgroundCopyJobCredentialTarget.Proxy, "user", "mypwd"), Throws.Nothing);
Assert.That(job.Credentials.Count, Is.EqualTo(1));
Assert.That(job.Credentials[BackgroundCopyJobCredentialScheme.Digest, BackgroundCopyJobCredentialTarget.Proxy].UserName, Is.EqualTo("user"));
var ch = new System.Net.WebHeaderCollection() { "A1:Test", "A2:Prova" };
Assert.That(() => job.CustomHeaders = ch, Throws.Nothing);
Assert.That(job.CustomHeaders, Has.Count.EqualTo(2));
Assert.That(job.Description, Is.EqualTo(job.GetDefVal<string>(nameof(job.Description))));
Assert.That(() => job.Description = jname, Throws.Nothing);
Assert.That(job.Description, Is.EqualTo(jname));
Assert.That(job.DisableNotifications, Is.EqualTo(job.GetDefVal<bool>(nameof(job.DisableNotifications))));
Assert.That(() => job.DisableNotifications = true, Throws.Nothing);
Assert.That(job.DisableNotifications, Is.EqualTo(true));
Assert.That(job.DynamicContent, Is.EqualTo(job.GetDefVal<bool>(nameof(job.DynamicContent))));
Assert.That(() => job.DynamicContent = true, Throws.Nothing);
Assert.That(job.DynamicContent, Is.EqualTo(true));
Assert.That(job.ErrorCount, Is.EqualTo(job.GetDefVal<int>(nameof(job.ErrorCount))));
Assert.That(job.HighPerformance, Is.EqualTo(job.GetDefVal<bool>(nameof(job.HighPerformance))));
Assert.That(() => job.HighPerformance = true, Throws.Nothing);
Assert.That(job.HighPerformance, Is.EqualTo(true));
Assert.That(job.ID, Is.Not.EqualTo(Guid.Empty));
Assert.That(job.JobType, Is.EqualTo(BackgroundCopyJobType.Download));
Assert.That(job.LastError, Is.Null);
Assert.That(job.MaxDownloadSize, Is.EqualTo(job.GetDefVal<ulong>(nameof(job.MaxDownloadSize))));
Assert.That(() => job.MaxDownloadSize = 1000, Throws.Nothing);
Assert.That(job.MaxDownloadSize, Is.EqualTo(1000));
Assert.That(job.MaximumDownloadTime, Is.EqualTo(job.GetDefVal<TimeSpan>(nameof(job.MaximumDownloadTime))));
Assert.That(() => job.MaximumDownloadTime = TimeSpan.FromDays(1), Throws.Nothing);
Assert.That(job.MaximumDownloadTime, Is.EqualTo(TimeSpan.FromDays(1)));
Assert.That(job.MinimumNotificationInterval, Is.EqualTo(job.GetDefVal<TimeSpan>(nameof(job.MinimumNotificationInterval))));
Assert.That(() => job.MinimumNotificationInterval = TimeSpan.FromSeconds(10), Throws.Nothing);
Assert.That(job.MinimumNotificationInterval, Is.EqualTo(TimeSpan.FromSeconds(10)));
Assert.That(job.MinimumRetryDelay, Is.EqualTo(job.GetDefVal<TimeSpan>(nameof(job.MinimumRetryDelay))));
Assert.That(() => job.MinimumRetryDelay = TimeSpan.FromSeconds(1000), Throws.Nothing);
Assert.That(job.MinimumRetryDelay, Is.EqualTo(TimeSpan.FromSeconds(1000)));
Assert.That(job.NoProgressTimeout, Is.EqualTo(job.GetDefVal<TimeSpan>(nameof(job.NoProgressTimeout))));
Assert.That(() => job.NoProgressTimeout = TimeSpan.FromDays(10), Throws.Nothing);
Assert.That(job.NoProgressTimeout, Is.EqualTo(TimeSpan.FromDays(10)));
Assert.That(job.NotificationCLSID, Is.EqualTo(job.GetDefVal<Guid>(nameof(job.NotificationCLSID))));
var guid = Guid.NewGuid();
Assert.That(() => job.NotificationCLSID = guid, Throws.Nothing);
Assert.That(job.NotificationCLSID, Is.EqualTo(guid));
Assert.That(job.NotifyProgram, Is.EqualTo(job.GetDefVal<string>(nameof(job.NotifyProgram))));
var str = "\"cmd.exe\" echo Bob";
Assert.That(() => job.NotifyProgram = str, Throws.Nothing);
Assert.That(job.NotifyProgram, Is.EqualTo(str));
Assert.That(job.OnDemand, Is.EqualTo(job.GetDefVal<bool>(nameof(job.OnDemand))));
Assert.That(() => job.OnDemand = true, Throws.Nothing);
Assert.That(job.OnDemand, Is.EqualTo(true));
Assert.That(job.Owner, Is.EqualTo(System.Security.Principal.WindowsIdentity.GetCurrent().User));
Assert.That(job.OwnerIntegrityLevel, Is.EqualTo(8192));
Assert.That(job.OwnerIsElevated, Is.EqualTo(false));
Assert.That(job.Priority, Is.EqualTo(job.GetDefVal<BackgroundCopyJobPriority>(nameof(job.Priority))));
Assert.That(() => job.Priority = BackgroundCopyJobPriority.Low, Throws.Nothing);
Assert.That(job.Priority, Is.EqualTo(BackgroundCopyJobPriority.Low));
Assert.That(job.Progress.BytesTransferred, Is.EqualTo(0));
Assert.That(job.Proxy, Is.EqualTo(job.GetDefVal<System.Net.WebProxy>(nameof(job.Proxy))));
Assert.That(() => job.Proxy = new System.Net.WebProxy("http://1.1.1.1"), Throws.Nothing);
Assert.That(job.Proxy.Address, Is.EqualTo(new Uri("http://1.1.1.1")));
Assert.That(job.State, Is.EqualTo(BackgroundCopyJobState.Suspended));
Assert.That(job.SecurityOptions, Is.EqualTo(job.GetDefVal<BackgroundCopyJobSecurity>(nameof(job.SecurityOptions))));
Assert.That(() => job.SecurityOptions = BackgroundCopyJobSecurity.CheckCRL, Throws.Nothing);
Assert.That(job.SecurityOptions, Is.EqualTo(BackgroundCopyJobSecurity.CheckCRL));
Assert.That(job.TransferBehavior, Is.EqualTo(job.GetDefVal<BackgroundCopyCost>(nameof(job.TransferBehavior))));
Assert.That(() => job.TransferBehavior = BackgroundCopyCost.Unrestricted, Throws.Nothing);
Assert.That(job.TransferBehavior, Is.EqualTo(BackgroundCopyCost.Unrestricted));
Assert.That(job.UseStoredCredentials, Is.EqualTo(job.GetDefVal<BackgroundCopyJobCredentialTarget>(nameof(job.UseStoredCredentials))));
Assert.That(() => job.UseStoredCredentials = BackgroundCopyJobCredentialTarget.Proxy, Throws.Nothing);
Assert.That(job.UseStoredCredentials, Is.EqualTo(BackgroundCopyJobCredentialTarget.Proxy));
Assert.That(job.Credentials.Remove(BackgroundCopyJobCredentialScheme.Digest, BackgroundCopyJobCredentialTarget.Proxy), Is.True);
Assert.That(job.Credentials.Count, Is.EqualTo(0));
Assert.That(job.CreationTime, Is.LessThan(DateTime.Now));
Assert.That(job.ModificationTime, Is.LessThan(DateTime.Now));
Assert.That(job.TransferCompletionTime, Has.Property("Year").EqualTo(1600));
}
}
public static class Ext
{
public static T GetDefVal<T>(this object obj, string prop)
{
var pi = obj.GetType().GetProperty(prop, typeof(T));
var attr = (System.ComponentModel.DefaultValueAttribute)pi.GetCustomAttributes(typeof(System.ComponentModel.DefaultValueAttribute), false).FirstOrDefault();
if (attr?.Value == null) return default(T);
if (attr.Value is T) return (T)attr.Value;
var cval = (attr.Value as IConvertible)?.ToType(typeof(T), null);
return cval != null ? (T)cval : throw new InvalidCastException();
}
}
}

View File

@ -116,6 +116,7 @@
<Compile Include="Shell\ShellItemTests.cs" />
<Compile Include="Shell\ShellFolderTests.cs" />
<Compile Include="Shell\ShellItemPropStoreTests.cs" />
<Compile Include="System\BackgroundCopyTests.cs" />
<Compile Include="System\VirtualDiskTests.cs" />
<Compile Include="UI\Controls\CredentialsDialogTests.cs" />
</ItemGroup>
@ -128,6 +129,10 @@
<Project>{ef88cf8c-e737-4bb1-bbf9-f47bf65547e0}</Project>
<Name>Vanara.PInvoke.AclUI</Name>
</ProjectReference>
<ProjectReference Include="..\PInvoke\BITS\Vanara.PInvoke.BITS.csproj">
<Project>{ef53ed51-c141-4525-a6b7-41109ee5f416}</Project>
<Name>Vanara.PInvoke.BITS</Name>
</ProjectReference>
<ProjectReference Include="..\PInvoke\ComCtl32\Vanara.PInvoke.ComCtl32.csproj">
<Project>{291af9b0-51e8-4e4a-972b-77310a7a0c06}</Project>
<Name>Vanara.PInvoke.ComCtl32</Name>