File: src\PushToBuildStorage.cs
Web Access
Project: src\src\Microsoft.DotNet.Build.Tasks.Feed\Microsoft.DotNet.Build.Tasks.Feed.csproj (Microsoft.DotNet.Build.Tasks.Feed)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.CodeDom;
using System.IO;
using System.Linq;
using System.Security;
using System.Xml.Linq;
using Microsoft.Arcade.Common;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.DotNet.Build.Manifest;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
 
namespace Microsoft.DotNet.Build.Tasks.Feed
{
    /// <summary>
    /// This task is used to gather a set of artifacts produced by a build and move them
    /// to a staging location, either locally or in Azure DevOps, as well as produce a manifest describing those
    /// artifacts. This is used today in the following scenarios:
    /// - VMR builds, for each repo that builds within a vertical. the artifacts are copied to a local directory,
    ///   and the manifest is produced. These artifacts are used for downstream repos in the vertical. In this case:
    ///     - PushToLocalStorage is set to true
    ///     - AssetsLocalStorageDir, ShippingPackagesLocalStorageDir, NonShippingPackagesLocalStorageDir, AssetManifestsLocalStorageDir,
    ///       and PdbArtifactsLocalStorageDir are set.
    ///     - FutureArtifactName and FutureArtifactPublishBasePath are not set.
    ///     - Publishing version should be v3 or v4.
    /// - VMR builds, at the end of a vertical. Or non-VMR builds using v4 publishing. In these cases, the artifacts are copied to a staging directory,
    ///   ready for upload as pipeline artifacts. The manifest is produced for all artifacts that should exit the vertical,
    ///   and the assets future pipeline artifact location is recorded in the manifest based on 
    ///   FutureArtifactName and FutureArtifactPublishBasePath. In this case,
    ///     - PushToLocalStorage is set to true
    ///     - AssetsLocalStorageDir, ShippingPackagesLocalStorageDir, NonShippingPackagesLocalStorageDir, AssetManifestsLocalStorageDir,
    ///       and PdbArtifactsLocalStorageDir are set.
    ///     - FutureArtifactName and FutureArtifactPublishBasePath are set.
    ///     - Publishing version should be v4.
    /// - Non-VMR builds, with publishing V3. In this case, the artifacts are uploaded to Azure DevOps using logging commands to
    ///   a set of well known artifact locations. PackageArtifacts, PdbArtifacts, BlobArtifacts, and AssetManifests.
    ///   When this scenario is active,
    ///     - PushToLocalStorage is set to false
    ///     - AssetsLocalStorageDir, ShippingPackagesLocalStorageDir, NonShippingPackagesLocalStorageDir, AssetManifestsLocalStorageDir.
    ///     - PdbArtifactsLocalStorageDir must be set if PDBs are present.
    ///     - FutureArtifactName and FutureArtifactPublishBasePath are not set.
    ///     - Publishing version should be v3.
    /// </summary>
    public class PushToBuildStorage : MSBuildTaskBase
    {
        [Required]
        public ITaskItem[] ItemsToPush { get; set; }
 
        public string ManifestRepoName { get; set; }
 
        public string ManifestRepoUri { get; set; }
 
        public string ManifestBuildId { get; set; } = "no build id provided";
 
        public string ManifestBranch { get; set; }
 
        public string ManifestCommit { get; set; }
 
        /// <summary>
        /// Indicates the source of the artifacts. For a VMR build, the `ManifestRepoName` is dotnet/dotnet,
        /// while the `ManifestRepoOrigin` corresponds to the actual product repository.
        /// </summary>
        public string ManifestRepoOrigin { get; set; }
 
        public string[] ManifestBuildData { get; set; }
 
        public string AzureDevOpsCollectionUri { get; set; }
 
        public string AzureDevOpsProject { get; set; }
 
        public int AzureDevOpsBuildId { get; set; }
        
        // Sign* parameters are deprecated and no longer used.
 
        public ITaskItem[] ItemsToSign { get; set; }
 
        public ITaskItem[] StrongNameSignInfo { get; set; }
 
        public ITaskItem[] FileSignInfo { get; set; }
 
        public ITaskItem[] FileExtensionSignInfo { get; set; }
 
        public ITaskItem[] CertificatesSignInfo { get; set; }
 
        public string AssetManifestPath { get; set; }
 
        public bool IsStableBuild { get; set; }
 
        public bool IsReleaseOnlyPackageVersion { get; set; }
 
        /// <summary>
        /// Represents where assets should be copied locally, either for staging for upload
        /// or for propagation to another phase of the VMR build.
        /// </summary>
        public string AssetsLocalStorageDir { get; set; }
 
        /// <summary>
        /// Represents where shipping packages should be copied locally, either for staging for upload
        /// or for propagation to another phase of the VMR build.
        /// </summary>
        public string ShippingPackagesLocalStorageDir { get; set; }
 
        /// <summary>
        /// Represents where nonshipping packages should be copied locally, either for staging for upload
        /// or for propagation to another phase of the VMR build.
        /// </summary>
        public string NonShippingPackagesLocalStorageDir { get; set; }
 
        /// <summary>
        /// Represents where asset manifests should be copied locally, either for staging for upload
        /// or for propagation to another phase of the VMR build.
        /// </summary>
        public string AssetManifestsLocalStorageDir { get; set; }
 
        /// <summary>
        /// Represents where pdb artifacts should be copied locally, either for staging for upload
        /// or for propagation to another phase of the VMR build.
        /// 
        /// NOTE: In non-VMR builds, this represents the location of the PDBs that are copied
        /// to before uploading to the PDBArtifacts dir.
        /// </summary>
        public string PdbArtifactsLocalStorageDir { get; set; }
 
        /// <summary>
        /// If true, the artifacts will be copied to the local storage directories.
        /// This is also true if the publishing version is V4.
        /// </summary>
        public bool PushToLocalStorage { get; set; }
 
        /// <summary>
        /// The final path for any packages published to <see cref="ShippingPackagesLocalStorageDir"/>
        /// or <see cref="NonShippingPackagesLocalStorageDir"/> should have the artifact's RepoOrigin
        /// appended as a subfolder to the published path.
        /// </summary>
        public bool PreserveRepoOrigin { get; set; }
 
        /// <summary>
        /// The visibility of the artifacts to put in the manifest.
        /// </summary>
        public ITaskItem[] ArtifactVisibilitiesToPublish { get; set; }
 
        /// <summary>
        /// Which version should the build manifest be tagged with.
        /// By default he latest version is used.
        /// </summary>
        public int PublishingVersion { get; set; }
 
        /// <summary>
        /// Gets or sets a value that indicates whether to use hard links for the copied files
        /// rather than copy the files, if it's possible to do so.
        /// </summary>
        public bool UseHardlinksIfPossible { get; set; } = true;
 
        /// <summary>
        /// Under v4 publishing, the artifact name is the name of the file that will be published to the blob feed.
        /// </summary>
        public string FutureArtifactName { get; set; }
 
        /// <summary>
        /// Under v4 publishing, the path that will be published to FutureArtifactName. If FutureArtifactName is passed,
        /// then FutureArtifactPublishBasePath must also be passed.
        /// </summary>
        public string FutureArtifactPublishBasePath { get; set; }
 
        private bool ShouldLocallyStageArtifacts { get => PushToLocalStorage || _publishingVersion >= PublishingInfraVersion.V4; }
 
        private PublishingInfraVersion _publishingVersion = PublishingInfraVersion.Latest;
 
        private IFileSystem _fileSystem;
 
        public override void ConfigureServices(IServiceCollection collection)
        {
            collection.TryAddSingleton<IBlobArtifactModelFactory, BlobArtifactModelFactory>();
            collection.TryAddSingleton<IPackageArtifactModelFactory, PackageArtifactModelFactory>();
            collection.TryAddSingleton<IPdbArtifactModelFactory, PdbArtifactModelFactory>();
            collection.TryAddSingleton<IBuildModelFactory, BuildModelFactory>();
            collection.TryAddSingleton<IFileSystem>(provider => new PushToBuildStorageFileSystem(UseHardlinksIfPossible, Log));
            collection.TryAddSingleton<IPackageArchiveReaderFactory, PackageArchiveReaderFactory>();
            collection.TryAddSingleton<INupkgInfoFactory, NupkgInfoFactory>();
            collection.TryAddSingleton(Log);
        }
 
        public bool PrepAndValidateTask()
        {
            if (PublishingVersion != default)
            {
                if (!Enum.IsDefined(typeof(PublishingInfraVersion), PublishingVersion))
                {
                    Log.LogError($"Invalid publishing version '{PublishingVersion}'");
                    return false;
                }
                _publishingVersion = (PublishingInfraVersion)PublishingVersion;
            }
 
            // Must always have something to push, or to create a manifest with.
            if (ItemsToPush == null || ItemsToPush.Length == 0)
            {
                Log.LogError($"ItemsToPush is not specified.");
                return false;
            }
 
            if (ShouldLocallyStageArtifacts)
            {
                if (string.IsNullOrEmpty(AssetsLocalStorageDir) ||
                    string.IsNullOrEmpty(ShippingPackagesLocalStorageDir) ||
                    string.IsNullOrEmpty(NonShippingPackagesLocalStorageDir) ||
                    string.IsNullOrEmpty(PdbArtifactsLocalStorageDir) ||
                    string.IsNullOrEmpty(AssetManifestsLocalStorageDir))
                {
                    Log.LogError("AssetsLocalStorageDir, ShippingPackagesLocalStorageDir, NonShippingPackagesLocalStorageDir, PdbArtifactsLocalStorageDir and AssetManifestsLocalStorageDir need " +
                        "to be specified if PublishToLocalStorage is set to true or V4 publishing is enabled");
                    return false;
                }
            }
 
            // Validation of parameters specific to a publishing version
            switch (_publishingVersion)
            {
                case PublishingInfraVersion.UnsupportedV1:
                case PublishingInfraVersion.UnsupportedV2:
                    Log.LogError($"Publishing version '{_publishingVersion}' is not supported.");
                    return false;
                case PublishingInfraVersion.V3:
                    if (!string.IsNullOrEmpty(FutureArtifactName) ||
                        !string.IsNullOrEmpty(FutureArtifactPublishBasePath))
                    {
                        Log.LogError($"FutureArtifactName and FutureArtifactPublishBasePath are not supported in publishing version '{_publishingVersion}'.");
                        return false;
                    }
                    break;
                case PublishingInfraVersion.V4:
                    if (string.IsNullOrEmpty(FutureArtifactName) !=
                        string.IsNullOrEmpty(FutureArtifactPublishBasePath))
                    {
                        Log.LogError($"FutureArtifactName and FutureArtifactPublishBasePath must be both be specified if either is specified.");
                        return false;
                    }
                    break;
                default:
                    throw new ArgumentOutOfRangeException(nameof(_publishingVersion), _publishingVersion, null);
            }
 
            if (ShouldLocallyStageArtifacts)
            {
                Log.LogMessage(MessageImportance.High, "Performing push to local artifacts storage.");
            }
            else
            {
                Log.LogMessage(MessageImportance.High, "Performing push to Azure DevOps artifacts storage.");
            }
 
            return true;
        }
 
        public bool ExecuteTask(IFileSystem fileSystem,
            IBlobArtifactModelFactory blobArtifactModelFactory,
            IPackageArtifactModelFactory packageArtifactModelFactory,
            IPdbArtifactModelFactory pdbArtifactModelFactory,
            IBuildModelFactory buildModelFactory)
        {
            _fileSystem = fileSystem;
 
            try
            {
                if (!PrepAndValidateTask())
                {
                    return false;
                }
 
                var artifactVisibilities = GetVisibilitiesToPublish(ArtifactVisibilitiesToPublish);
 
                var buildModel = buildModelFactory.CreateModel(
                    ItemsToPush,
                    artifactVisibilities,
                    ManifestBuildId,
                    ManifestBuildData,
                    !string.IsNullOrEmpty(ManifestRepoName) ? ManifestRepoName : ManifestRepoUri,
                    ManifestBranch,
                    ManifestCommit,
                    ManifestRepoOrigin,
                    _publishingVersion <= PublishingInfraVersion.V3 ? IsStableBuild : false,
                    _publishingVersion,
                    _publishingVersion <= PublishingInfraVersion.V3 ? IsReleaseOnlyPackageVersion : false);
 
                if (buildModel == null)
                {
                    Log.LogError($"Failed to construct build model from input artifacts.");
                    return false;
                }
 
                foreach (var package in buildModel.Artifacts.Packages)
                {
                    if (!fileSystem.FileExists(package.OriginalFile))
                    {
                        Log.LogError($"Could not find file {package.OriginalFile}.");
                        continue;
                    }
 
                    LocallyStageArtifactsOrPushToAzDO(package);
                }
 
                foreach (var blobArtifact in buildModel.Artifacts.Blobs)
                {
                    if (!fileSystem.FileExists(blobArtifact.OriginalFile))
                    {
                        Log.LogError($"Could not find file {blobArtifact.OriginalFile}.");
                        continue;
                    }
 
                    LocallyStageArtifactsOrPushToAzDO(blobArtifact);
                }
 
                // We allow users to not specify a PdbArtifactsLocalStorageDir to avoid breaking
                // some repos. Check now that it exists.
                // Note that if publishing version was v4, or if we are pushing to local storage,
                // then this was already checked.
                if (string.IsNullOrEmpty(PdbArtifactsLocalStorageDir) && buildModel.Artifacts.Pdbs.Count > 0)
                {
                    Log.LogError($"PdbArtifactsLocalStorageDir must be specified if PDBs are present.");
                    return false;
                }
 
                foreach (var pdbArtifact in buildModel.Artifacts.Pdbs)
                {
                    if (!fileSystem.FileExists(pdbArtifact.OriginalFile))
                    {
                        Log.LogError($"Could not find file {pdbArtifact.OriginalFile}.");
                        continue;
                    }
                    LocallyStageArtifactsOrPushToAzDO(pdbArtifact);
                }
 
                if (_publishingVersion <= PublishingInfraVersion.V3 && !ShouldLocallyStageArtifacts && buildModel.Artifacts.Pdbs.Any())
                {
                    // Upload the full set of PDBs
                    Log.LogMessage(MessageImportance.High,
                        $"##vso[artifact.upload containerfolder=PdbArtifacts;artifactname=PdbArtifacts]{PdbArtifactsLocalStorageDir}");
                }
 
                // Write the manifest, then create an artifact for it.
                Log.LogMessage(MessageImportance.High, $"Writing build manifest file '{AssetManifestPath}'...");
                fileSystem.WriteToFile(AssetManifestPath, buildModel.ToXml().ToString(SaveOptions.DisableFormatting));
 
                // Generate an artifact for the asset manifest and push it to storage.
                AssetManifestModel assetManifestModel = new AssetManifestModel
                {
                    OriginalFile = AssetManifestPath,
                    Id = _fileSystem.GetFileName(AssetManifestPath)
                };
                LocallyStageArtifactsOrPushToAzDO(assetManifestModel);
            }
            catch (Exception e)
            {
                Log.LogErrorFromException(e, true);
            }
 
            return !Log.HasLoggedErrors;
        }
 
        private void LocallyStageArtifactsOrPushToAzDO(ArtifactModel artifactModel)
        {
            string originalArtifactPath = artifactModel.OriginalFile;
            string artifactDestinationPath = originalArtifactPath;
 
            if (ShouldLocallyStageArtifacts)
            {
                artifactDestinationPath = PushArtifactToLocalStorage(artifactModel, originalArtifactPath);
            }
            else if (_publishingVersion == PublishingInfraVersion.V3)
            {
                PushArtifactToBuildStorage(artifactModel, originalArtifactPath);
            }
 
            // If using V4, then record the pipeline artifact location of the asset.
            if (_publishingVersion == PublishingInfraVersion.V4 && !string.IsNullOrEmpty(FutureArtifactPublishBasePath))
            {
                try
                {
                    string relativePath = _fileSystem.GetRelativePath(FutureArtifactPublishBasePath, artifactDestinationPath);
                    // Set the pipeline artifact path to the relative path.
                    // This path should be in unix-style path form.
                    artifactModel.PipelineArtifactName = FutureArtifactName;
                    artifactModel.PipelineArtifactPath = relativePath.Replace(@"\", "/");
                }
                catch (ArgumentException)
                {
                    Log.LogError($"Could not determine relative path from '{FutureArtifactPublishBasePath}' to '{artifactDestinationPath}'.");
                }
            }
        }
 
 
        /// <summary>
        /// Copies the artifact to the locally specified storage directory.
        /// </summary>
        /// <param name="artifactModel">Artifact model</param>
        /// <param name="originalArtifactPath">Original path to the artifact</param>
        /// <param name="filename"></param>
        /// <returns>Target path that the artifact was copied to.</returns>
        /// <exception cref="ArgumentOutOfRangeException">Invalid publishing version.</exception>
        private string PushArtifactToLocalStorage(ArtifactModel artifactModel, string originalArtifactPath)
        {
            string filename = _fileSystem.GetFileName(originalArtifactPath);
            string artifactDestinationPath;
            switch (artifactModel)
            {
                case AssetManifestModel _:
                    artifactDestinationPath = _fileSystem.PathCombine(AssetManifestsLocalStorageDir, filename);
                    EnsureDirectoryAndCopyFile(originalArtifactPath, artifactDestinationPath);
                    break;
 
                case PackageArtifactModel _:
                    string packageDestinationDirectory = artifactModel.NonShipping
                        ? NonShippingPackagesLocalStorageDir
                        : ShippingPackagesLocalStorageDir;
 
                    if (PreserveRepoOrigin)
                    {
                        packageDestinationDirectory = _fileSystem.PathCombine(packageDestinationDirectory, artifactModel.RepoOrigin);
                    }
                    artifactDestinationPath = _fileSystem.PathCombine(packageDestinationDirectory, filename);
                    EnsureDirectoryAndCopyFile(originalArtifactPath, artifactDestinationPath);
                    break;
 
                case BlobArtifactModel _:
                    string relativeBlobPath = artifactModel.Id;
                    artifactDestinationPath = _fileSystem.PathCombine(
                                            AssetsLocalStorageDir,
                                            string.IsNullOrEmpty(relativeBlobPath) ? filename : relativeBlobPath);
 
                    EnsureDirectoryAndCopyFile(originalArtifactPath, artifactDestinationPath);
                    break;
 
                case PdbArtifactModel _:
                    string relativePdbPath = artifactModel.Id;
                    artifactDestinationPath = _fileSystem.PathCombine(
                                                PdbArtifactsLocalStorageDir,
                                                string.IsNullOrEmpty(relativePdbPath) ? filename : relativePdbPath);
 
                    EnsureDirectoryAndCopyFile(originalArtifactPath, artifactDestinationPath);
                    break;
 
                default:
                    throw new ArgumentOutOfRangeException(nameof(artifactModel));
            }
 
            return artifactDestinationPath;
 
            void EnsureDirectoryAndCopyFile(string artifactPath, string artifactDestinationPath)
            {
                _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(artifactDestinationPath));
                _fileSystem.CopyFile(artifactPath, artifactDestinationPath, true);
            }
        }
 
        /// <summary>
        /// Pushes artifacts to build storage using logging commands, with the exception of PDBs, which are staged for upload as a unit.
        /// </summary>
        /// <param name="artifactModel">Artifact</param>
        /// <param name="originalArtifactPath">Original path to the artifact</param>
        /// <exception cref="ArgumentOutOfRangeException">Publishing version was out of range</exception>
        private void PushArtifactToBuildStorage(ArtifactModel artifactModel, string originalArtifactPath)
        {
            switch (artifactModel)
            {
                case AssetManifestModel _:
                    Log.LogMessage(MessageImportance.High,
                        $"##vso[artifact.upload containerfolder=AssetManifests;artifactname=AssetManifests]{originalArtifactPath}");
                    break;
                case PackageArtifactModel _:
                    Log.LogMessage(MessageImportance.High,
                        $"##vso[artifact.upload containerfolder=PackageArtifacts;artifactname=PackageArtifacts]{originalArtifactPath}");
                    break;
                case BlobArtifactModel _:
                    Log.LogMessage(MessageImportance.High,
                        $"##vso[artifact.upload containerfolder=BlobArtifacts;artifactname=BlobArtifacts]{originalArtifactPath}");
                    break;
                case PdbArtifactModel _:
                    string pdbArtifactTarget = _fileSystem.PathCombine(PdbArtifactsLocalStorageDir, artifactModel.Id);
                    _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(pdbArtifactTarget));
                    // Copy the PDB artifact to the temp local dir.
                    _fileSystem.CopyFile(originalArtifactPath, pdbArtifactTarget, true);
                    break;
                default:
                    throw new ArgumentOutOfRangeException(nameof(artifactModel));
            }
        }
 
        private static ArtifactVisibility GetVisibilitiesToPublish(ITaskItem[] allowedVisibilities)
        {
            if (allowedVisibilities is null || allowedVisibilities.Length == 0)
            {
                return ArtifactVisibility.External;
            }
 
            ArtifactVisibility visibility = 0;
 
            foreach (var item in allowedVisibilities)
            {
                if (Enum.TryParse(item.ItemSpec, true, out ArtifactVisibility parsedVisibility))
                {
                    visibility |= parsedVisibility;
                }
                else
                {
                    throw new ArgumentException($"Invalid visibility: {item.ItemSpec}");
                }
            }
 
            return visibility;
        }
    }
 
    /// <summary>
    /// File system with some extra hard linking functionality useful for the PushToBuildStorage task.
    /// Specifically, we override CopyFile to enable hard linking
    /// </summary>
    class PushToBuildStorageFileSystem : FileSystem
    {
        bool _enableHardLinking = false;
        private readonly TaskLoggingHelper _log;
 
        public PushToBuildStorageFileSystem(
            bool enableHardLinking,
            TaskLoggingHelper log)
        {
            _enableHardLinking = enableHardLinking;
            _log = log;
        }
 
        /// <summary>
        /// 
        /// </summary>
        /// <param name="basePath">Base path</param>
        /// <param name="targetPath">Target path</param>
        /// <returns>Relative path from basePath to targetPath</returns>
        /// <exception cref="ArgumentNullException"></exception>
        public override string GetRelativePath(string basePath, string targetPath)
        {
            // Resolve the basePath and targetPath to absolute paths.
            basePath = GetFullPath(basePath);
            targetPath = GetFullPath(targetPath);
 
            if (targetPath.IndexOf(basePath) != 0)
            {
                throw new ArgumentException("targetPath is not relative to basePath");
            }
 
            return targetPath.Substring(basePath.Length).TrimStart('/', '\\');
        }
 
        /// <summary>
        /// The below method implementation is copied from msbuild's Copy task and adjusted.
        /// </summary>
        /// <param name="sourceFileName">Source file for the copy</param>
        /// <param name="destFileName">Destination file for the copy</param>
        /// <param name="overwrite">Overwrite?</param>
        public override void CopyFile(string sourceFileName, string destFileName, bool overwrite)
        {
            FileInfo destFile = new(destFileName);
 
            if (_enableHardLinking)
            {
                // NativeMethods.MakeHardLink cannot overwrite an existing file or link
                // so we need to delete the existing entry before we create the hard link.
                if (destFile.Exists && !destFile.IsReadOnly)
                {
                    try
                    {
                        DeleteFile(destFile.FullName);
                    }
                    catch (Exception ex) when (IsIoRelatedException(ex))
                    {
                    }
                }
 
                _log.LogMessage(MessageImportance.Normal, $"Creating hard link to copy \"{sourceFileName}\" to \"{destFileName}\".");
 
                string errorMessage = string.Empty;
                if (!NativeMethods.MakeHardLink(destFileName, sourceFileName, ref errorMessage))
                {
                    _log.LogMessage(MessageImportance.Normal, $"Could not use a link to copy \"{sourceFileName}\" to \"{destFileName}\". Copying the file instead. {errorMessage}");
                    File.Copy(sourceFileName, destFileName, overwrite);
                }
            }
            else
            {
                File.Copy(sourceFileName, destFileName, overwrite);
            }
 
            // If the destinationFile file exists, then make sure it's read-write.
            // The File.Copy command copies attributes, but our copy needs to
            // leave the file writeable.
            if (new FileInfo(sourceFileName).IsReadOnly)
            {
                // Ensure the read-only attribute on the specified file is off, so
                // the file is writeable.
                if (destFile.Exists)
                {
                    if (destFile.IsReadOnly)
                    {
                        _log.LogMessage(MessageImportance.Low, $"Removing read-only attribute from \"{destFile.FullName}\".");
                        File.SetAttributes(destFile.FullName, FileAttributes.Normal);
                    }
                }
            }
 
            // Determine whether the exception is file-IO related.
            static bool IsIoRelatedException(Exception e)
            {
                // These all derive from IOException
                //     DirectoryNotFoundException
                //     DriveNotFoundException
                //     EndOfStreamException
                //     FileLoadException
                //     FileNotFoundException
                //     PathTooLongException
                //     PipeException
                return e is UnauthorizedAccessException
                    || e is NotSupportedException
                    || (e is ArgumentException && !(e is ArgumentNullException))
                    || e is SecurityException
                    || e is IOException;
            }
        }
    }
}