大文件复制期间的进度(Copy-Item和Write-Progress?)

93

有没有办法在PowerShell中复制一个非常大的文件(从一个服务器到另一个服务器)并显示进度?

有一些解决方案可以使用Write-Progress与循环结合使用来复制多个文件并显示进度。 但是,我似乎找不到任何可以显示单个文件进度的东西。

有什么想法吗?

12个回答

1

这是一篇旧帖子,但我认为它可能会对其他人有所帮助。
使用FileStream的解决方案很优雅且有效,但速度非常慢。
我认为使用其他程序,如robocopy.exe,会削弱powershell的目的。
这甚至是Monad宣言中的动机之一。
因此,我打开了Microsoft.PowerShell.Management中的Copy-Item cmdlet,并在最后调用了kernel32.dll中的CopyFileEx。

在CopyFileEx签名中,有一个参数接受回调以提供进度信息。
pinvoke.net上,有一个很好的示例,展示了如何将此函数和回调封送到委托中。
我稍微修改了一下,以便我们可以从PS脚本本身提供委托。

相信我,当我说这个时,我并没有期望它会工作:D(我真的从椅子上跳了下来)。

而且它的速度要快得多。
以下是代码:

function Copy-File {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory, Position = 0)]
        [string]$Path,

        [Parameter(Mandatory, Position = 1)]
        [string]$Destination
    )

    $signature = @'
    namespace Utilities {

        using System;
        using System.Runtime.InteropServices;
    
        public class FileSystem {
            
            [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            [return: MarshalAs(UnmanagedType.Bool)]
            static extern bool CopyFileEx(
                string lpExistingFileName,
                string lpNewFileName,
                CopyProgressRoutine lpProgressRoutine,
                IntPtr lpData,
                ref Int32 pbCancel,
                CopyFileFlags dwCopyFlags
            );
        
            delegate CopyProgressResult CopyProgressRoutine(
            long TotalFileSize,
            long TotalBytesTransferred,
            long StreamSize,
            long StreamBytesTransferred,
            uint dwStreamNumber,
            CopyProgressCallbackReason dwCallbackReason,
            IntPtr hSourceFile,
            IntPtr hDestinationFile,
            IntPtr lpData);
        
            int pbCancel;
        
            public enum CopyProgressResult : uint
            {
                PROGRESS_CONTINUE = 0,
                PROGRESS_CANCEL = 1,
                PROGRESS_STOP = 2,
                PROGRESS_QUIET = 3
            }
        
            public enum CopyProgressCallbackReason : uint
            {
                CALLBACK_CHUNK_FINISHED = 0x00000000,
                CALLBACK_STREAM_SWITCH = 0x00000001
            }
        
            [Flags]
            enum CopyFileFlags : uint
            {
                COPY_FILE_FAIL_IF_EXISTS = 0x00000001,
                COPY_FILE_RESTARTABLE = 0x00000002,
                COPY_FILE_OPEN_SOURCE_FOR_WRITE = 0x00000004,
                COPY_FILE_ALLOW_DECRYPTED_DESTINATION = 0x00000008
            }
        
            public void CopyWithProgress(string oldFile, string newFile, Func<long, long, long, long, uint, CopyProgressCallbackReason, System.IntPtr, System.IntPtr, System.IntPtr, CopyProgressResult> callback)
            {
                CopyFileEx(oldFile, newFile, new CopyProgressRoutine(callback), IntPtr.Zero, ref pbCancel, CopyFileFlags.COPY_FILE_RESTARTABLE);
            }
        }
    }
'@

    Add-Type -TypeDefinition $signature

    [Func[long, long, long, long, System.UInt32, Utilities.FileSystem+CopyProgressCallbackReason, System.IntPtr, System.IntPtr, System.IntPtr, Utilities.FileSystem+CopyProgressResult]]$copyProgressDelegate = {

        param($total, $transfered, $streamSize, $streamByteTrans, $dwStreamNumber, $reason, $hSourceFile, $hDestinationFile, $lpData)

        Write-Progress -Activity "Copying file" -Status "$Path ~> $Destination. $([Math]::Round(($transfered/1KB), 2))KB/$([Math]::Round(($total/1KB), 2))KB." -PercentComplete (($transfered / $total) * 100)
    }

    $fileName = [System.IO.Path]::GetFileName($Path)
    $destFileName = [System.IO.Path]::GetFileName($Destination)
    if ([string]::IsNullOrEmpty($destFileName) -or $destFileName -notlike '*.*') {
        if ($Destination.EndsWith('\')) {
            $destFullName = "$Destination$fileName"
        }
        else {
            $destFullName = "$Destination\$fileName"
        }
    }

    $wrapper = New-Object Utilities.FileSystem
    $wrapper.CopyWithProgress($Path, $destFullName, $copyProgressDelegate)
}

希望能有所帮助。
愉快地编写脚本吧!

更新:

同样的事情,但使用CopyFile2。

try {
    Add-Type -TypeDefinition @'
namespace Utilities {
    using System;
    using System.Text;
    using System.Runtime.InteropServices;

    public delegate COPYFILE2_MESSAGE_ACTION CopyFile2ProgressRoutine(
        [In] COPYFILE2_MESSAGE pMessage,
        [In, Optional] IntPtr pvCallbackContext
    );

    [Flags]
    public enum CopyFlags : uint {
        COPY_FILE_FAIL_IF_EXISTS = 0x00000001,
        COPY_FILE_RESTARTABLE = 0x00000002,
        COPY_FILE_OPEN_SOURCE_FOR_WRITE = 0x00000004,
        COPY_FILE_ALLOW_DECRYPTED_DESTINATION = 0x00000008,
        COPY_FILE_COPY_SYMLINK = 0x00000800,
        COPY_FILE_NO_BUFFERING = 0x00001000,
        COPY_FILE_REQUEST_SECURITY_PRIVILEGES = 0x00002000,
        COPY_FILE_RESUME_FROM_PAUSE = 0x00004000,
        COPY_FILE_NO_OFFLOAD = 0x00040000,
        COPY_FILE_REQUEST_COMPRESSED_TRAFFIC = 0x10000000
    }
    
    public enum COPYFILE2_MESSAGE_ACTION : uint {
        COPYFILE2_PROGRESS_CONTINUE,
        COPYFILE2_PROGRESS_CANCEL,
        COPYFILE2_PROGRESS_STOP,
        COPYFILE2_PROGRESS_QUIET,
        COPYFILE2_PROGRESS_PAUSE
    }

    public enum COPYFILE2_MESSAGE_TYPE : uint {
        COPYFILE2_CALLBACK_NONE,
        COPYFILE2_CALLBACK_CHUNK_STARTED,
        COPYFILE2_CALLBACK_CHUNK_FINISHED,
        COPYFILE2_CALLBACK_STREAM_STARTED,
        COPYFILE2_CALLBACK_STREAM_FINISHED,
        COPYFILE2_CALLBACK_POLL_CONTINUE,
        COPYFILE2_CALLBACK_ERROR,
        COPYFILE2_CALLBACK_MAX
    }

    public enum COPYFILE2_COPY_PHASE : uint {
        COPYFILE2_PHASE_NONE,
        COPYFILE2_PHASE_PREPARE_SOURCE,
        COPYFILE2_PHASE_PREPARE_DEST,
        COPYFILE2_PHASE_READ_SOURCE,
        COPYFILE2_PHASE_WRITE_DESTINATION,
        COPYFILE2_PHASE_SERVER_COPY,
        COPYFILE2_PHASE_NAMEGRAFT_COPY,
        COPYFILE2_PHASE_MAX
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct ULARGE_INTEGER {
        public Int64 QuadPart;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct _ChunkStarted {
        public uint          dwStreamNumber;
        public uint          dwReserved;
        public IntPtr         hSourceFile;
        public IntPtr         hDestinationFile;
        public ULARGE_INTEGER uliChunkNumber;
        public ULARGE_INTEGER uliChunkSize;
        public ULARGE_INTEGER uliStreamSize;
        public ULARGE_INTEGER uliTotalFileSize;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct _ChunkFinished {
        public uint          dwStreamNumber;
        public uint          dwFlags;
        public IntPtr         hSourceFile;
        public IntPtr         hDestinationFile;
        public ULARGE_INTEGER uliChunkNumber;
        public ULARGE_INTEGER uliChunkSize;
        public ULARGE_INTEGER uliStreamSize;
        public ULARGE_INTEGER uliStreamBytesTransferred;
        public ULARGE_INTEGER uliTotalFileSize;
        public ULARGE_INTEGER uliTotalBytesTransferred;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct _StreamStarted {
        public uint          dwStreamNumber;
        public uint          dwReserved;
        public IntPtr         hSourceFile;
        public IntPtr         hDestinationFile;
        public ULARGE_INTEGER uliStreamSize;
        public ULARGE_INTEGER uliTotalFileSize;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct _StreamFinished {
        public uint          dwStreamNumber;
        public uint          dwReserved;
        public IntPtr         hSourceFile;
        public IntPtr         hDestinationFile;
        public ULARGE_INTEGER uliStreamSize;
        public ULARGE_INTEGER uliStreamBytesTransferred;
        public ULARGE_INTEGER uliTotalFileSize;
        public ULARGE_INTEGER uliTotalBytesTransferred;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct _PollContinue {
        public uint dwReserved;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct _Error {
        COPYFILE2_COPY_PHASE CopyPhase;
        uint                dwStreamNumber;
        IntPtr              hrFailure;
        uint                dwReserved;
        ULARGE_INTEGER       uliChunkNumber;
        ULARGE_INTEGER       uliStreamSize;
        ULARGE_INTEGER       uliStreamBytesTransferred;
        ULARGE_INTEGER       uliTotalFileSize;
        ULARGE_INTEGER       uliTotalBytesTransferred;
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct COPYFILE2_MESSAGE {
        [FieldOffset(0)]
        public COPYFILE2_MESSAGE_TYPE Type;

        [FieldOffset(1)]
        public uint dwPadding;

        [FieldOffset(2)]
        public _ChunkStarted ChunkStarted;

        [FieldOffset(2)]
        public _ChunkFinished ChunkFinished;

        [FieldOffset(2)]
        public _StreamStarted StreamStarted;

        [FieldOffset(2)]
        public _StreamFinished StreamFinished;

        [FieldOffset(2)]
        public _PollContinue PollContinue;

        [FieldOffset(2)]
        public _Error Error;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct COPYFILE2_EXTENDED_PARAMETERS {
        public uint dwSize;
        public CopyFlags dwCopyFlags;
        public bool pfCancel;
        public CopyFile2ProgressRoutine pProgressRoutine;
        public IntPtr pvCallbackContext;
    }

    public class FileSystem {

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        public static extern uint CopyFile2(
            string pwszExistingFileName,
            string pwszNewFileName,
            COPYFILE2_EXTENDED_PARAMETERS pExtendedParameters
        );

        public static void CopyFileEx(string filePath, string destination, Func<COPYFILE2_MESSAGE, IntPtr, COPYFILE2_MESSAGE_ACTION> callback) {
            COPYFILE2_EXTENDED_PARAMETERS extParams = new();
            extParams.dwSize = Convert.ToUInt32(Marshal.SizeOf(extParams));
            extParams.dwCopyFlags = CopyFlags.COPY_FILE_NO_BUFFERING | CopyFlags.COPY_FILE_COPY_SYMLINK;
            extParams.pProgressRoutine = new CopyFile2ProgressRoutine(callback);
            extParams.pvCallbackContext = IntPtr.Zero;

            uint result = CopyFile2(filePath, destination, extParams);
            if (result != 0)
                throw new SystemException(result.ToString());
        }
    }
}
'@
}
catch { }

[Func[
    Utilities.COPYFILE2_MESSAGE,
    IntPtr,
    Utilities.COPYFILE2_MESSAGE_ACTION
]]$delegate = {

    param([Utilities.COPYFILE2_MESSAGE]$message, $extArgs, $result)

    if ($message.Type -eq [Utilities.COPYFILE2_MESSAGE_TYPE]::COPYFILE2_CALLBACK_CHUNK_FINISHED) {
        Write-Progress -Activity 'Copying file.' -Status 'Copying...' -PercentComplete (($message.ChunkFinished.uliTotalFileSize.QuadPart / $message.ChunkFinished.uliStreamBytesTransferred.QuadPart) * 100)
    }
}

if (Test-Path -Path C:\CopyFile2TestDestination -PathType Container) { [void](mkdir C:\CopyFile2TestDestination) }
[Utilities.FileSystem]::CopyFileEx('C:\superTest.dat', 'C:\CopyFile2TestDestination\superTestCopy.dat', $delegate)

看起来很不错,但我刚学到CopyFileEx不支持源目录。CopyFile2在这方面更好吗? - gth
1
事实上,我的原始想法是直接管理目录,使用'New-Item'或'System.IO.Directory'。CopyFile2不支持进度回调,但它是一个很好的资源。 - FranciscoNabas
你的代码是我找到的用于CopyFileEx的最佳示例 - 我非常感激!看起来CopyFile2支持进度例程。当我检测到目录作为源项目时,我可能会尝试递归调用CopyFileEx,并继续深入。希望不会遇到任何限制。 - gth
有趣!我得试试。谢谢! - FranciscoNabas
1
是的!!! 它有效。 https://github.com/FranciscoNabas/PowerShellPublic/blob/main/CopyFile2.ps1 由于某种奇怪的原因,ChunkFinished.uliTotalBytesTransferred和ChunkFinished.uliTotalFileSize是相反的。 我在编组联合时可能错过了一些东西。 - FranciscoNabas

0

Trevor Sullivan 在他的文章中详细介绍了如何将名为 Copy-ItemWithProgress 的命令添加到 PowerShell 上,以替代 Robocopy。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接