复制多个文件并报告进度 C#

3

在查看多个问题/答案后,我无法找到我的问题的解决方案。我记得我从StackOverflow中的某个问题获取了这段代码,它完美地运行,但仅适用于一个文件。我想要的是多个文件。

这是原始的CopyTo函数:

    public static void CopyTo(this FileInfo file, FileInfo destination, Action<int> progressCallback)
    {
        const int bufferSize = 1024 * 1024;  //1MB
        byte[] buffer = new byte[bufferSize], buffer2 = new byte[bufferSize];
        bool swap = false;
        int progress = 0, reportedProgress = 0, read = 0;

        long len = file.Length;
        float flen = len;
        Task writer = null;

        using (var source = file.OpenRead())
        using (var dest = destination.OpenWrite())
        {
            //dest.SetLength(source.Length);
            for (long size = 0; size < len; size += read)
            {
                if ((progress = ((int)((size / flen) * 100))) != reportedProgress)
                    progressCallback(reportedProgress = progress);
                read = source.Read(swap ? buffer : buffer2, 0, bufferSize);
                writer?.Wait();  // if < .NET4 // if (writer != null) writer.Wait(); 
                writer = dest.WriteAsync(swap ? buffer : buffer2, 0, read);
                swap = !swap;
            }
            writer?.Wait();  //Fixed - Thanks @sam-hocevar
        }
    }

以下是我启动文件复制过程的方法:

                var ficheiro = ficheirosCopia.ElementAt(x);
                var _source = new FileInfo(ficheiro.Key);
                var _destination = new FileInfo(ficheiro.Value);

                if (_destination.Exists)
                {
                    _destination.Delete();
                }

                Task.Run(() =>
                {
                    _source.CopyTo(_destination, perc => Dispatcher.Invoke(() => progressBar.SetProgress(perc)));
                }).GetAwaiter().OnCompleted(() => MessageBox.Show("File Copied!"));

当我只复制一个文件时,这个方法非常有效。但是我需要复制多个文件。所以我开始做一些修改:

    public static void CopyTo(Dictionary<string, string> files, Action<int> progressCallback)
    {
        int globalProgress = 0, globalReportedProgress = 0, globalRead = 0;

        for (var x = 0; x < files.Count; x++)
        {
            var item = files.ElementAt(x);
            var file = new FileInfo(item.Key);
            var destination = new FileInfo(item.Value);

            const int bufferSize = 1024 * 1024;  //1MB
            byte[] buffer = new byte[bufferSize], buffer2 = new byte[bufferSize];
            bool swap = false;
            int progress = 0, reportedProgress = 0, read = 0;

            long len = file.Length;
            float flen = len;
            Task writer = null;

            using (var source = file.OpenRead())
            using (var dest = destination.OpenWrite())
            {
                for (long size = 0; size < len; size += read)
                {
                    if ((progress = ((int)((size / flen) * 100))) != reportedProgress)
                        progressCallback(reportedProgress = progress);
                    read = source.Read(swap ? buffer : buffer2, 0, bufferSize);
                    writer?.Wait();  // if < .NET4 // if (writer != null) writer.Wait(); 
                    writer = dest.WriteAsync(swap ? buffer : buffer2, 0, read);
                    swap = !swap;
                }
                writer?.Wait();  //Fixed - Thanks @sam-hocevar
            }
        }

    }

当然,这段代码有很多错误,但我不知道应该如何解决。

主要目标是为多个平铺启动单个任务,并具有全局复制的进度回调。将一个字典(它已经在代码的其他部分创建)作为参数接收。


也许这是一个愚蠢的问题,但为什么你不使用文件对象的FileCopy方法呢?只是为了你的进度条吗? - nabuchodonossor
你的意思是只针对进度条吗?能否详细说明一下? - rgomez
我认为他的意思是:如果你不需要每复制一定大小的数据就更新进度条,那么你可以在循环中使用File.Copy()作为解决方案。 - Niklas
请查看BackgroundWorker。这将有助于你的实现。 - Jehof
是的,但我需要。我认为解决方案是在复制每个单独文件并开始复制之前,我需要为所有文件长度声明一个全局计数,并开始报告和接收,而不是单个文件大小。 - rgomez
@niklas 你是正确的。 - nabuchodonossor
1个回答

0
我想到了两种方法,一种是在每个文件后报告进度,另一种是在每 n 个字节后报告进度。
namespace StackOverflow41750117CopyProgress
{
    using System;
    using System.Collections.Generic;
    using System.IO;

    public class Batch
    {
        private bool _overwrite;

        /// <summary>
        /// Initializes a new instance of the <see cref="Batch"/> class.
        /// </summary>
        /// <param name="overwrite">
        /// True to overwrite the destination file if it already exists (default),
        /// false to throw an exception if the destination file already exists.
        /// </param>
        public Batch(bool overwrite = true)
        {
            this._overwrite = overwrite;
        }

        /// <summary>
        /// Copies the files, reporting progress once per file.
        /// </summary>
        /// <param name="filesToCopy">
        /// A dictionary with the paths of the source files as its keys, and the path to the destination file as its values.
        /// </param>
        /// <param name="progressCallback">
        /// A callback which accepts two Int64 parameters - the number of bytes copied so far, and the total number of bytes to copy.
        /// </param>
        public void CopyReportingPerFile(Dictionary<string, string> filesToCopy, Action<long, long> progressCallback)
        {
            var bytesToCopy = this.GetTotalFileSize(filesToCopy);
            long totalBytesCopied = 0;
            foreach (var copy in filesToCopy)
            {
                File.Copy(copy.Key, copy.Value, this._overwrite);
                totalBytesCopied += new FileInfo(copy.Key).Length;
                progressCallback(totalBytesCopied, bytesToCopy);
            }
        }

        /// <summary>
        /// Copies the files, reporting progress once per read/write operation.
        /// </summary>
        /// <param name="filesToCopy">
        /// A dictionary with the paths of the source files as its keys, and the path to the destination file as its values.
        /// </param>
        /// <param name="progressCallback">
        /// A callback which accepts two Int64 parameters - the number of bytes copied so far, and the total number of bytes to copy.
        /// </param>
        public void CopyReportingPerBuffer(Dictionary<string, string> filesToCopy, Action<long, long> progressCalllback)
        {
            var bytesToCopy = this.GetTotalFileSize(filesToCopy);
            var bufferSize = 1024 * 1024 * 50;
            var buffer = new byte[bufferSize];
            var span = new Span<byte>(buffer);
            long totalBytesCopied = 0;
            foreach (var copy in filesToCopy)
            {
                using (var source = File.OpenRead(copy.Key))
                using (var destination = File.OpenWrite(copy.Value))
                {
                    int bytesRead = 0;
                    do
                    {
                        // The Read method returns 0 once we've reached the end of the file
                        bytesRead = source.Read(span);
                        destination.Write(span);
                        totalBytesCopied += bytesRead;
                        progressCalllback(totalBytesCopied, bytesToCopy);
                    } while (bytesRead > 0);

                    source.Close();
                    destination.Close();
                }
            }
        }

        private long GetTotalFileSize(Dictionary<string, string> filesToCopy)
        {
            long bytesToCopy = 0;
            foreach (var filename in filesToCopy.Keys)
            {
                var fileInfo = new FileInfo(filename);
                bytesToCopy += fileInfo.Length;
            }

            return bytesToCopy;
        }
    }
}

使用方法:

namespace StackOverflow41750117CopyProgress
{
    using System;
    using System.Collections.Generic;
    using System.IO;

    public class Program
    {
        public static void Main(string[] args)
        {
            var filesToCopy = new Dictionary<string, string>();
            filesToCopy.Add(@"C:\temp\1.mp4", @"C:\temp\1copy.mp4");
            filesToCopy.Add(@"C:\temp\2.mp4", @"C:\temp\2copy.mp4");
            filesToCopy.Add(@"C:\temp\3.mp4", @"C:\temp\3copy.mp4");
            filesToCopy.Add(@"C:\temp\4.mp4", @"C:\temp\4copy.mp4");
            filesToCopy.Add(@"C:\temp\5.mp4", @"C:\temp\5copy.mp4");
            filesToCopy.Add(@"C:\temp\6.mp4", @"C:\temp\6copy.mp4");
            filesToCopy.Add(@"C:\temp\7.mp4", @"C:\temp\7copy.mp4");

            // Make sure the destination files don't already exist
            foreach (var copy in filesToCopy)
            {
                File.Delete(copy.Value);
            }

            var batch = new Batch();
            Console.WriteLine($"Started  {DateTime.Now}");
            batch.CopyReportingPerFile(filesToCopy, (bytesCopied, bytesToCopy) => Console.WriteLine($"Copied {bytesCopied} bytes of {bytesToCopy}"));
            //batch.CopyReportingPerBuffer(filesToCopy, (bytesCopied, bytesToCopy) => Console.WriteLine($"Copied {bytesCopied} bytes of {bytesToCopy}"));
            Console.WriteLine($"Finished {DateTime.Now}");
        }
    }
}

一些观察...

  • 每个文件报告进度一次更容易实现,但不符合问题的要求,并且如果您正在复制少量大文件,则响应性不太好。
  • 使用File.Copy保留了原始文件的修改日期,将文件读入内存然后写入它们则不会。
  • 将缓冲区大小从1MB增加到10MB和50MB会增加内存使用量并提高性能,尽管大部分性能改进似乎是由于在我的progressCallback中较少调用Console.Writeline而不是增加磁盘I/O速度。
  • 性能和进度报告频率之间的最佳平衡取决于您的情况和运行该过程的机器规格,但我发现50MB缓冲区大约每秒报告一次进度。
  • 请注意,使用Span<byte>而不是byte[]作为数据读入和写出的缓冲区 - 这消除了我的代码跟踪文件中当前位置的需要(这是我今天学到的新东西)。

我知道我回答这个问题有点晚了,但希望有人会发现这个有用。


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