Подтвердить что ты не робот

Могу ли я показать прогресс копирования файла с помощью FileInfo.CopyTo() в .NET?

Я создал утилиту копирования в С# (.NET 2.0 Framework), которая копирует файлы, каталоги и рекурсивные подкаталоги и т.д. Программа имеет графический интерфейс, который показывает текущий файл, который копируется, текущий номер файла (последовательность) общее количество копируемых файлов и процент выполненных операций копирования. Существует также индикатор выполнения, основанный на текущих файлах/файлах.

Моя проблема связана с копированием больших файлов. Мне не удалось найти способ указать общий ход копирования большого файла (используя мою текущую структуру классов, которая утилизирует метод FileInfo.CopyTo). В качестве обходного пути я разделил операции копирования файлов и графического интерфейса на свои собственные потоки и настроил визуальный сигнал, чтобы показать, что работа выполняется. По крайней мере, пользователь знает, что программа не была заморожена и все еще копирует файлы.

Было бы лучше показать прогресс, основанный на общем количестве байтов, или иметь какой-либо тип события, который запускается из метода FileInfo.CopyTo, который указывает общее количество байтов, скопированных из текущего файла.

Мне известно о свойстве FileInfo.Length, поэтому я уверен, что MacGuyver использует мое собственное событие, основанное на этом, и имеет обработчик на стороне GUI вещей, читающих обновления (возможно, на основе проверки свойство FileInfo.Length объекта назначения с использованием некоторого типа таймера?).

Кто-нибудь знает, как это сделать, что я пропускаю. Если я смогу избежать этого, я бы предпочел не переписывать свой класс, чтобы копировать байты через поток и отслеживать его таким образом (хотя я думаю, что мог бы застрять в этом маршруте).

Спасибо заблаговременно

PS - Я привязан к платформе .NET 2.0, поэтому любое решение, которое требует функций, доступных только в >= 3.0, для меня не является вариантом.

PPS - я открыт для решений в любом разнообразии .NET, а не только С#.

4b9b3361

Ответ 1

FileInfo.CopyTo в основном представляет собой оболочку API-интерфейса Win32 API "CopyFile" в файле kernel32.dll. Этот метод не поддерживает обратный вызов прогресса.

Однако метод CopyFileEx делает, и вы можете написать свою собственную оболочку .NET вокруг него через несколько минут, как описано здесь: http://www.pinvoke.net/default.aspx/kernel32.CopyFileEx

Ответ 2

Я также использовал реализацию, представленную в отмеченном ответе. Однако я создал оболочку для предоставления API-интерфейса nicer ™ для использования с .NET.

Применение:

XCopy.Copy(networkFile.FullPath, temporaryFilename, true, true, (o, pce) => 
{
    worker.ReportProgress(pce.ProgressPercentage, networkFile);
});

Реализация

/// <summary>
/// PInvoke wrapper for CopyEx
/// http://msdn.microsoft.com/en-us/library/windows/desktop/aa363852.aspx
/// </summary>
public class XCopy
{
    public static void Copy(string source, string destination, bool overwrite, bool nobuffering)
    {
         new XCopy().CopyInternal(source, destination, overwrite, nobuffering, null);            
    }

    public static void Copy(string source, string destination, bool overwrite, bool nobuffering, EventHandler<ProgressChangedEventArgs> handler)
    {            
         new XCopy().CopyInternal(source, destination, overwrite, nobuffering, handler);            
    }

    private event EventHandler Completed;
    private event EventHandler<ProgressChangedEventArgs> ProgressChanged;

    private int IsCancelled;
    private int FilePercentCompleted;
    private string Source;
    private string Destination;        

    private XCopy()
    {
        IsCancelled = 0;
    }

    private void CopyInternal(string source, string destination, bool overwrite, bool nobuffering, EventHandler<ProgressChangedEventArgs> handler)
    {
        try
        {
            CopyFileFlags copyFileFlags = CopyFileFlags.COPY_FILE_RESTARTABLE;
            if (!overwrite)
                copyFileFlags |= CopyFileFlags.COPY_FILE_FAIL_IF_EXISTS;

            if (nobuffering)
                copyFileFlags |= CopyFileFlags.COPY_FILE_NO_BUFFERING;

            Source = source;
            Destination = destination;

            if (handler != null)
                ProgressChanged += handler;

            bool result = CopyFileEx(Source, Destination, new CopyProgressRoutine(CopyProgressHandler), IntPtr.Zero, ref IsCancelled, copyFileFlags);
            if (!result)
                throw new Win32Exception(Marshal.GetLastWin32Error());
        }
        catch (Exception)
        {
            if (handler != null)
                ProgressChanged -= handler;

            throw;
        }
    }

    private void OnProgressChanged(double percent)
    {
        // only raise an event when progress has changed
        if ((int)percent > FilePercentCompleted)
        {
            FilePercentCompleted = (int)percent;

            var handler = ProgressChanged;
            if (handler != null)
                handler(this, new ProgressChangedEventArgs((int)FilePercentCompleted, null));
        }
    }

    private void OnCompleted()
    {
        var handler = Completed;
        if (handler != null)
            handler(this, EventArgs.Empty);
    }

    #region PInvoke

    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool CopyFileEx(string lpExistingFileName, string lpNewFileName, CopyProgressRoutine lpProgressRoutine, IntPtr lpData, ref Int32 pbCancel, CopyFileFlags dwCopyFlags);

    private delegate CopyProgressResult CopyProgressRoutine(long TotalFileSize, long TotalBytesTransferred, long StreamSize, long StreamBytesTransferred, uint dwStreamNumber, CopyProgressCallbackReason dwCallbackReason,
                                                    IntPtr hSourceFile, IntPtr hDestinationFile, IntPtr lpData);

    private enum CopyProgressResult : uint
    {
        PROGRESS_CONTINUE = 0,
        PROGRESS_CANCEL = 1,
        PROGRESS_STOP = 2,
        PROGRESS_QUIET = 3
    }

    private enum CopyProgressCallbackReason : uint
    {
        CALLBACK_CHUNK_FINISHED = 0x00000000,
        CALLBACK_STREAM_SWITCH = 0x00000001
    }

    [Flags]
    private enum CopyFileFlags : uint
    {
        COPY_FILE_FAIL_IF_EXISTS = 0x00000001,
        COPY_FILE_NO_BUFFERING = 0x00001000,
        COPY_FILE_RESTARTABLE = 0x00000002,
        COPY_FILE_OPEN_SOURCE_FOR_WRITE = 0x00000004,
        COPY_FILE_ALLOW_DECRYPTED_DESTINATION = 0x00000008
    }

    private CopyProgressResult CopyProgressHandler(long total, long transferred, long streamSize, long streamByteTrans, uint dwStreamNumber,
                                                   CopyProgressCallbackReason reason, IntPtr hSourceFile, IntPtr hDestinationFile, IntPtr lpData)
    {
        if (reason == CopyProgressCallbackReason.CALLBACK_CHUNK_FINISHED)
            OnProgressChanged((transferred / (double)total) * 100.0);

        if (transferred >= total)
            OnCompleted();

        return CopyProgressResult.PROGRESS_CONTINUE;
    }

    #endregion

}

Ответ 3

Я знаю, что немного опоздал на вечеринку, но я сделал обертку для CopyFileEx, которая возвращает Task и принимает CancellationToken и IProgress<double>. К сожалению, он не будет работать в среде .NET 2.0, но для тех, кто использует 4.5, это позволяет использовать ключевое слово await.

public static class FileEx
{
    public static Task CopyAsync(string sourceFileName, string destFileName)
    {
        return CopyAsync(sourceFileName, destFileName, CancellationToken.None);
    }

    public static Task CopyAsync(string sourceFileName, string destFileName, CancellationToken token)
    {
        return CopyAsync(sourceFileName, destFileName, token, null);
    }

    public static Task CopyAsync(string sourceFileName, string destFileName, IProgress<double> progress)
    {
        return CopyAsync(sourceFileName, destFileName, CancellationToken.None, progress);
    }

    public static Task CopyAsync(string sourceFileName, string destFileName, CancellationToken token, IProgress<double> progress)
    {
        int pbCancel = 0;
        CopyProgressRoutine copyProgressHandler;
        if (progress != null)
        {
            copyProgressHandler = (total, transferred, streamSize, streamByteTrans, dwStreamNumber, reason, hSourceFile, hDestinationFile, lpData) =>
            {
                progress.Report((double)transferred / total * 100);
                return CopyProgressResult.PROGRESS_CONTINUE;
            };
        }
        else
        {
            copyProgressHandler = EmptyCopyProgressHandler;
        }
        token.ThrowIfCancellationRequested();
        var ctr = token.Register(() => pbCancel = 1);
        var copyTask = Task.Run(() =>
        {
            try
            {
                CopyFileEx(sourceFileName, destFileName, copyProgressHandler, IntPtr.Zero, ref pbCancel, CopyFileFlags.COPY_FILE_RESTARTABLE);
                token.ThrowIfCancellationRequested();
            }
            finally
            {
                ctr.Dispose();
            }
        }, token);
        return copyTask;
    }

    private static CopyProgressResult EmptyCopyProgressHandler(long total, long transferred, long streamSize, long streamByteTrans, uint dwStreamNumber, CopyProgressCallbackReason reason, IntPtr hSourceFile, IntPtr hDestinationFile, IntPtr lpData)
    {
        return CopyProgressResult.PROGRESS_CONTINUE;
    }

    #region DLL Import

    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private 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);

    enum CopyProgressResult : uint
    {
        PROGRESS_CONTINUE = 0,
        PROGRESS_CANCEL = 1,
        PROGRESS_STOP = 2,
        PROGRESS_QUIET = 3
    }

    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
    }

    #endregion
}

Ответ 4

Ибо любовь Бога не реализует вашу собственную копию файла, используя потоки! API Win32 CopyFile API, упомянутый Гаспар, может использовать, например, DMA, тогда как я бы поставил доллары на пончики, которые написал код, не будет достаточно "умным", чтобы это сделать.

CopyFileEx будет относиться к вам правильно, или вы можете реализовать BackgroundWorker, который следит за ростом размера целевого файла и обновляет индикатор выполнения, используя эту информацию. Последний метод избавляет вас от PInvoke, но первый, вероятно, немного чист в конечном итоге.

Ответ 5

Для таких вещей я вернулся к Shell32 (или это ShellUI? Я уже не знаю). Это дает вам родное диалоговое окно Windows, которое пользователи используют для операций копирования. Я предполагаю, что это заменит ваш уже существующий диалог, чтобы он не был правильным ответом для вас, но полезно помнить для этих сценариев "в крайнем случае".

Microsoft.VisualBasic.FileIO.FileSystem.CopyFile(
    srcPath, 
    dstPath, 
    Microsoft.VisualBasic.FileIO.UIOption.AllDialogs,    
    Microsoft.VisualBasic.FileIO.UICancelOption.ThrowException
);

Да, вы должны ссылаться на сборку Microsoft.VisualBasic. Я вырос до люблю эту сборку.

Ответ 6

Благодаря @Gasper и @Dennis для указания метода CopyFileEx. Я продлил ответ dennis с прерванной копией

    /// <summary>
    /// Type indicates how the copy gets completed.
    /// </summary>
    internal enum CopyCompletedType
    {
        Succeeded,
        Aborted,
        Exception
    }

/// <summary>
/// Event arguments for file copy 
/// </summary>
internal class FileCopyEventArgs : EventArgs
{
    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="type">type of the copy completed type enum</param>
    /// <param name="exception">exception if any</param>
    public FileCopyEventArgs(CopyCompletedType type, Exception exception)
    {
        Type = type;
        Exception = exception;
    }

    /// <summary>
    /// Type of the copy completed type
    /// </summary>
    public CopyCompletedType Type
    {
        get;
        private set;

    }

    /// <summary>
    /// Exception if any happend during copy.
    /// </summary>
    public Exception Exception
    {
        get;
        private set;
    }

}

/// <summary>
/// PInvoke wrapper for CopyEx
/// http://msdn.microsoft.com/en-us/library/windows/desktop/aa363852.aspx
/// </summary>
internal class XCopy
{

    private int IsCancelled;
    private int FilePercentCompleted;

    public XCopy()
    {
        IsCancelled = 0;
    }

    /// <summary>
    /// Copies the file asynchronously
    /// </summary>
    /// <param name="source">the source path</param>
    /// <param name="destination">the destination path</param>
    /// <param name="nobuffering">Bufferig status</param>
    /// <param name="handler">Event handler to do file copy.</param>
    public void CopyAsync(string source, string destination, bool nobuffering)
    {
        try
        {
            //since we needed an async copy ..
            Action action = new Action(
                () => CopyInternal(source, destination, nobuffering)
                    );
            Task task = new Task(action);
            task.Start();
        }
        catch (AggregateException ex)
        {
            //handle the inner exception since exception thrown from task are wrapped in
            //aggreate exception.
            OnCompleted(CopyCompletedType.Exception, ex.InnerException);
        }
        catch (Exception ex)
        {
            OnCompleted(CopyCompletedType.Exception, ex);
        }
    }

    /// <summary>
    /// Event which will notify the subscribers if the copy gets completed
    /// There are three scenarios in which completed event will be thrown when
    /// 1.Copy succeeded
    /// 2.Copy aborted.
    /// 3.Any exception occured.
    /// These information can be obtained from the Event args.
    /// </summary>
    public event EventHandler<FileCopyEventArgs> Completed;
    /// <summary>
    /// Event which will notify the subscribers if there is any progress change while copying.
    /// This will indicate the progress percentage in its event args.
    /// </summary>
    public event EventHandler<ProgressChangedEventArgs> ProgressChanged;

    /// <summary>
    /// Aborts the copy asynchronously and throws Completed event when done.
    /// User may not want to wait for completed event in case of Abort since 
    /// the event will tell that copy has been aborted.
    /// </summary>
    public void AbortCopyAsync()
    {
        Trace.WriteLine("Aborting the copy");
        //setting this will cancel an operation since we pass the
        //reference to copyfileex and it will periodically check for this.
        //otherwise also We can check for iscancelled on onprogresschanged and return 
        //Progress_cancelled .
        IsCancelled = 1;

        Action completedEvent = new Action(() =>
            {
                //wait for some time because we ll not know when IsCancelled is set , at what time windows stops copying.
                //so after sometime this may become valid .
                Thread.Sleep(500);
                //do we need to wait for some time and send completed event.
                OnCompleted(CopyCompletedType.Aborted);
                //reset the value , otherwise if we try to copy again since value is 1 , 
                //it thinks that its aborted and wont allow to copy.
                IsCancelled = 0;
            });

        Task completedTask = new Task(completedEvent);
        completedTask.Start();
    }


    /// <summary>
    /// Copies the file using asynchronos task
    /// </summary>
    /// <param name="source">the source path</param>
    /// <param name="destination">the destination path</param>
    /// <param name="nobuffering">Buffering status</param>
    /// <param name="handler">Delegate to handle Progress changed</param>
    private void CopyInternal(string source, string destination, bool nobuffering)
    {
        CopyFileFlags copyFileFlags = CopyFileFlags.COPY_FILE_RESTARTABLE;

        if (nobuffering)
        {
            copyFileFlags |= CopyFileFlags.COPY_FILE_NO_BUFFERING;
        }

        try
        {
            Trace.WriteLine("File copy started with Source: " + source + " and destination: " + destination);
            //call win32 api.
            bool result = CopyFileEx(source, destination, new CopyProgressRoutine(CopyProgressHandler), IntPtr.Zero, ref IsCancelled, copyFileFlags);
            if (!result)
            {
                //when ever we get the result as false it means some error occured so get the last win 32 error.
                throw new Win32Exception(Marshal.GetLastWin32Error());
            }
        }
        catch (Exception ex)
        {
            //the mesage will contain the requested operation was aborted when the file copy
            //was cancelled. so we explicitly check for that and do a graceful exit
            if (ex.Message.Contains("aborted"))
            {
                Trace.WriteLine("Copy aborted.");
            }
            else
            {
                OnCompleted(CopyCompletedType.Exception, ex.InnerException);
            }
        }
    }

    private void OnProgressChanged(double percent)
    {
        // only raise an event when progress has changed
        if ((int)percent > FilePercentCompleted)
        {
            FilePercentCompleted = (int)percent;

            var handler = ProgressChanged;
            if (handler != null)
            {
                handler(this, new ProgressChangedEventArgs((int)FilePercentCompleted, null));
            }
        }
    }

    private void OnCompleted(CopyCompletedType type, Exception exception = null)
    {
        var handler = Completed;
        if (handler != null)
        {
            handler(this, new FileCopyEventArgs(type, exception));
        }
    }

    #region PInvoke

    /// <summary>
    /// Delegate which will be called by Win32 API for progress change
    /// </summary>
    /// <param name="total">the total size</param>
    /// <param name="transferred">the transferrred size</param>
    /// <param name="streamSize">size of the stream</param>
    /// <param name="streamByteTrans"></param>
    /// <param name="dwStreamNumber">stream number</param>
    /// <param name="reason">reason for callback</param>
    /// <param name="hSourceFile">the source file handle</param>
    /// <param name="hDestinationFile">the destination file handle</param>
    /// <param name="lpData">data passed by users</param>
    /// <returns>indicating whether to continue or do somthing else.</returns>
    private CopyProgressResult CopyProgressHandler(long total, long transferred, long streamSize, long streamByteTrans, uint dwStreamNumber,
                                                   CopyProgressCallbackReason reason, IntPtr hSourceFile, IntPtr hDestinationFile, IntPtr lpData)
    {
        //when a chunk is finished call the progress changed.
        if (reason == CopyProgressCallbackReason.CALLBACK_CHUNK_FINISHED)
        {
            OnProgressChanged((transferred / (double)total) * 100.0);
        }

        //transfer completed
        if (transferred >= total)
        {
            if (CloseHandle(hDestinationFile))
            {
                OnCompleted(CopyCompletedType.Succeeded, null);
            }
            else
            {
                OnCompleted(CopyCompletedType.Exception,
                    new System.IO.IOException("Unable to close the file handle"));
            }
        }

        return CopyProgressResult.PROGRESS_CONTINUE;
    }
    [System.Runtime.InteropServices.DllImport("Kernel32")]
    private extern static Boolean CloseHandle(IntPtr handle);

    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool CopyFileEx(string lpExistingFileName, string lpNewFileName, CopyProgressRoutine lpProgressRoutine, IntPtr lpData, ref Int32 pbCancel, CopyFileFlags dwCopyFlags);

    private delegate CopyProgressResult CopyProgressRoutine(long TotalFileSize, long TotalBytesTransferred, long StreamSize, long StreamBytesTransferred, uint dwStreamNumber, CopyProgressCallbackReason dwCallbackReason,
                                                    IntPtr hSourceFile, IntPtr hDestinationFile, IntPtr lpData);

    private enum CopyProgressResult : uint
    {
        PROGRESS_CONTINUE = 0,
        PROGRESS_CANCEL = 1,
        PROGRESS_STOP = 2,
        PROGRESS_QUIET = 3
    }

    private enum CopyProgressCallbackReason : uint
    {
        CALLBACK_CHUNK_FINISHED = 0x00000000,
        CALLBACK_STREAM_SWITCH = 0x00000001
    }

    [Flags]
    private enum CopyFileFlags : uint
    {
        COPY_FILE_FAIL_IF_EXISTS = 0x00000001,
        COPY_FILE_NO_BUFFERING = 0x00001000,
        COPY_FILE_RESTARTABLE = 0x00000002,
        COPY_FILE_OPEN_SOURCE_FOR_WRITE = 0x00000004,
        COPY_FILE_ALLOW_DECRYPTED_DESTINATION = 0x00000008
    }

    #endregion

}

Клиенты могут создать объект класса XCopy и вызвать метод копирования/прерывания.