File: Move.cs
Web Access
Project: ..\..\..\src\Tasks\Microsoft.Build.Tasks.csproj (Microsoft.Build.Tasks.Core)
// 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.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
using Microsoft.Build.Utilities;
 
#nullable disable
 
namespace Microsoft.Build.Tasks
{
    /// <summary>
    /// Task to move one or more files.
    /// </summary>
    /// <remarks>
    /// This does not support moving directories (ie, xcopy)
    /// but this could restriction could be lifted as MoveFileEx,
    /// which is used here, supports it.
    /// </remarks>
    public class Move : TaskExtension, ICancelableTask, IIncrementalTask
    {
        /// <summary>
        /// Flags for MoveFileEx.
        ///
        /// </summary>
        private const NativeMethods.MoveFileFlags Flags = NativeMethods.MoveFileFlags.MOVEFILE_WRITE_THROUGH |    // Do not return until the Move is complete
                                                          NativeMethods.MoveFileFlags.MOVEFILE_REPLACE_EXISTING | // Replace any existing target
                                                          NativeMethods.MoveFileFlags.MOVEFILE_COPY_ALLOWED;      // Moving across volumes is allowed
 
        /// <summary>
        /// Whether we should cancel.
        /// </summary>
        private bool _canceling;
 
        /// <summary>
        /// List of files to move.
        /// </summary>
        [Required]
        public ITaskItem[] SourceFiles { get; set; }
 
        /// <summary>
        /// Destination folder for all the source files.
        /// </summary>
        public ITaskItem DestinationFolder { get; set; }
 
        /// <summary>
        /// Whether to overwrite files in the destination
        /// that have the read-only attribute set.
        /// Default is to not overwrite.
        /// </summary>
        public bool OverwriteReadOnlyFiles { get; set; }
 
        /// <summary>
        /// Destination files matching each of the source files.
        /// </summary>
        [Output]
        public ITaskItem[] DestinationFiles { get; set; }
 
        /// <summary>
        /// Subset that were successfully moved.
        /// </summary>
        [Output]
        public ITaskItem[] MovedFiles { get; private set; }
 
        /// <summary>
        /// Set question parameter for Move task.
        /// </summary>
        /// <remarks>Move can be chained A->B->C with location C as the final location.
        /// Incrementally, it is hard to question A->B if both files are gone.
        /// In short, question will always return false and author should use target inputs/outputs.</remarks>
        public bool FailIfNotIncremental { get; set; }
 
        /// <summary>
        /// Stop and return (in an undefined state) as soon as possible.
        /// </summary>
        public void Cancel()
        {
            _canceling = true;
        }
 
        /// <summary>
        /// Main entry point.
        /// </summary>
        public override bool Execute()
        {
            bool success = true;
 
            // If there are no source files then just return success.
            if (SourceFiles == null || SourceFiles.Length == 0)
            {
                DestinationFiles = Array.Empty<ITaskItem>();
                MovedFiles = Array.Empty<ITaskItem>();
                return true;
            }
 
            // There must be a DestinationFolder (either files or directory).
            if (DestinationFiles == null && DestinationFolder == null)
            {
                Log.LogErrorWithCodeFromResources("Move.NeedsDestination", "DestinationFiles", "DestinationDirectory");
                return false;
            }
 
            // There can't be two kinds of destination.
            if (DestinationFiles != null && DestinationFolder != null)
            {
                Log.LogErrorWithCodeFromResources("Move.ExactlyOneTypeOfDestination", "DestinationFiles", "DestinationDirectory");
                return false;
            }
 
            // If the caller passed in DestinationFiles, then its length must match SourceFiles.
            if (DestinationFiles != null && DestinationFiles.Length != SourceFiles.Length)
            {
                Log.LogErrorWithCodeFromResources("General.TwoVectorsMustHaveSameLength", DestinationFiles.Length, SourceFiles.Length, "DestinationFiles", "SourceFiles");
                return false;
            }
 
            // If the caller passed in DestinationFolder, convert it to DestinationFiles
            if (DestinationFiles == null)
            {
                DestinationFiles = new ITaskItem[SourceFiles.Length];
 
                for (int i = 0; i < SourceFiles.Length; ++i)
                {
                    // Build the correct path.
                    string destinationFile;
                    try
                    {
                        destinationFile = Path.Combine(DestinationFolder.ItemSpec, Path.GetFileName(SourceFiles[i].ItemSpec));
                    }
                    catch (ArgumentException e)
                    {
                        Log.LogErrorWithCodeFromResources("Move.Error", SourceFiles[i].ItemSpec, DestinationFolder.ItemSpec, e.Message, string.Empty);
 
                        // Clear the outputs.
                        DestinationFiles = Array.Empty<ITaskItem>();
                        return false;
                    }
 
                    // Initialize the DestinationFolder item.
                    DestinationFiles[i] = new TaskItem(destinationFile);
                }
            }
 
            // Build up the sucessfully moved subset
            var destinationFilesSuccessfullyMoved = new List<ITaskItem>();
 
            // Now that we have a list of DestinationFolder files, move from source to DestinationFolder.
            for (int i = 0; i < SourceFiles.Length && !_canceling; ++i)
            {
                string sourceFile = SourceFiles[i].ItemSpec;
                string destinationFile = DestinationFiles[i].ItemSpec;
 
                try
                {
                    if (!FailIfNotIncremental && MoveFileWithLogging(sourceFile, destinationFile))
                    {
                        SourceFiles[i].CopyMetadataTo(DestinationFiles[i]);
                        destinationFilesSuccessfullyMoved.Add(DestinationFiles[i]);
                    }
                    else
                    {
                        success = false;
                    }
                }
                catch (Exception e) when (ExceptionHandling.IsIoRelatedException(e))
                {
                    string lockedFileMessage = LockCheck.GetLockedFileMessage(sourceFile);
                    Log.LogErrorWithCodeFromResources("Move.Error", sourceFile, destinationFile, e.Message, lockedFileMessage);
                    success = false;
 
                    // Continue with the rest of the list
                }
            }
 
            // MovedFiles contains only the copies that were successful.
            MovedFiles = destinationFilesSuccessfullyMoved.ToArray();
 
            return success && !_canceling;
        }
 
        /// <summary>
        /// Makes the provided file writeable if necessary.
        /// </summary>
        private static void MakeWriteableIfReadOnly(string file)
        {
            var info = new FileInfo(file);
            if ((info.Attributes & FileAttributes.ReadOnly) != 0)
            {
                info.Attributes &= ~FileAttributes.ReadOnly;
            }
        }
 
        /// <summary>
        /// Move one file from source to destination. Create the target directory if necessary.
        /// </summary>
        /// <throws>IO related exceptions.</throws>
        private bool MoveFileWithLogging(
            string sourceFile,
            string destinationFile)
        {
            if (FileSystems.Default.DirectoryExists(destinationFile))
            {
                Log.LogErrorWithCodeFromResources("Move.DestinationIsDirectory", sourceFile, destinationFile);
                return false;
            }
 
            if (FileSystems.Default.DirectoryExists(sourceFile))
            {
                // If the source file passed in is actually a directory instead of a file, log a nice
                // error telling the user so.  Otherwise, .NET Framework's File.Move method will throw
                // an FileNotFoundException, which is not very useful to the user.
                Log.LogErrorWithCodeFromResources("Move.SourceIsDirectory", sourceFile);
                return false;
            }
 
            // Check the source exists.
            if (!FileSystems.Default.FileExists(sourceFile))
            {
                Log.LogErrorWithCodeFromResources("Move.SourceDoesNotExist", sourceFile);
                return false;
            }
 
            // We can't ovewrite a file unless it's writeable
            if (OverwriteReadOnlyFiles && FileSystems.Default.FileExists(destinationFile))
            {
                MakeWriteableIfReadOnly(destinationFile);
            }
 
            string destinationFolder = Path.GetDirectoryName(destinationFile);
 
            if (!string.IsNullOrEmpty(destinationFolder) && !FileSystems.Default.DirectoryExists(destinationFolder))
            {
                Log.LogMessageFromResources(MessageImportance.Normal, "Move.CreatesDirectory", destinationFolder);
                Directory.CreateDirectory(destinationFolder);
            }
 
            // Do not log a fake command line as well, as it's superfluous, and also potentially expensive
            Log.LogMessageFromResources(MessageImportance.Normal, "Move.FileComment", sourceFile, destinationFile);
 
            // We want to always overwrite any existing destination file.
            // Unlike File.Copy, File.Move does not have an overload to overwrite the destination.
            // We cannot simply delete the destination file first because possibly it is also the source!
            // Nor do we want to just do a Copy followed by a Delete, because for large files that will be slow.
            // We are forced to use Win32's MoveFileEx.
            bool result = NativeMethods.MoveFileEx(sourceFile, destinationFile, Flags);
 
            if (!result)
            {
                // It failed so we need a nice error message. Unfortunately
                // Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); and
                // throw new IOException((new Win32Exception(error)).Message)
                // do not produce great error messages (eg., "The operation succeeded" (!)).
                // For this reason the BCL has is own mapping in System.IO.__Error.WinIOError
                // which is unfortunately internal.
                // So try to get a nice message by using the BCL Move(), which will likely fail
                // and throw. Otherwise use the "correct" method.
                File.Move(sourceFile, destinationFile);
 
                // Apparently that didn't throw, so..
                Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
            }
 
            // If the destination file exists, then make sure it's read-write.
            // The File.Move command copies attributes, but our move needs to
            // leave the file writeable.
            if (FileSystems.Default.FileExists(destinationFile))
            {
                // Make it writable
                MakeWriteableIfReadOnly(destinationFile);
            }
 
            return true;
        }
    }
}