【问题标题】:Efficiently moving a file高效移动文件
【发布时间】:2020-02-01 00:12:11
【问题描述】:

我正在尝试将文件从一个目录移动到另一个目录,同时在我的 WPF 应用程序中看到一个进度条。

移动操作非常慢,我找不到让它更快的解决方案(测试移动 38 Mb 的速度是 2:30 分钟)但我不知道如何有效地移动它。我现在的移动方式有效,但效率极低。

public delegate void ProgressChangeDelegate(double percentage);
    public delegate void CompleteDelegate();

    class FileMover
    { 
        public string SourceFilePath { get; set; }
        public string DestFilePath { get; set; }

        public event ProgressChangeDelegate OnProgressChanged;
        public event CompleteDelegate OnComplete;

        public FileMover(string Source, string Dest)
        {
            SourceFilePath = Source;
            DestFilePath = Dest;

            OnProgressChanged += delegate { };
            OnComplete += delegate { };
        }

        public void Copy()
        {
            byte[] buffer = new byte[1024 * 1024]; // 1MB buffer
            using (FileStream source = new FileStream(SourceFilePath, FileMode.Open, FileAccess.Read))
            {
                long fileLength = source.Length;
                using (FileStream dest = new FileStream(DestFilePath, FileMode.CreateNew, FileAccess.Write))
                {
                    long totalBytes = 0;
                    int currentBlockSize = 0;

                    while ((currentBlockSize = source.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        totalBytes += currentBlockSize;
                        double percentage = (double) totalBytes * 100.0 / fileLength;

                        dest.Write(buffer, 0, currentBlockSize);
                        OnProgressChanged(percentage);
                    }
                }
            }
            OnComplete();
        }
    }
        private async void MoveFile(string source, string outDir)
        {
            if (!string.IsNullOrEmpty(outDir) && !string.IsNullOrEmpty(source))
            {
                //InputButtonText.Text = "Please be patient while we move your file.";
                //Task.Run(() => { new FileInfo(source).MoveTo(Path.Combine(outDir, Path.GetFileName(source))); }).GetAwaiter().OnCompleted(
                //    () =>
                //    {
                //        OutputScanned.ItemsSource = null;
                //        InputButtonText.Text = "Click to select a file";
                //    });

                var mover = new FileMover(source, Path.Combine(outDir, Path.GetFileName(source)));
                await Task.Run(() => { mover.Copy(); });

                mover.OnProgressChanged += percentage =>
                {
                    MoveProgress.Value = percentage;
                    InputButtonText.Text = percentage.ToString();
                };

                mover.OnComplete += () => { File.Delete(source); };
            }
        }

【问题讨论】:

  • 您似乎遇到了一些问题,您在这里具体寻求什么帮助?
  • 您需要将问题分解为单独的问题。一次专注于一个,为每个人提供良好的minimal reproducible example,以便人们可以提供帮助。也就是说,您不应该通过显式复制来移动文件。请改用System.IO.File.Move() 方法。只要文件在同一卷内移动,此操作就会非常快速且与文件大小无关。如果您尝试跨卷移动,文件将被复制而不是移动(如果您需要,您必须自己删除源),但该操作仍将有效地完成。
  • @PeterDuniho 如果他们选择使用System.IO.File.Move(),你对想要展示这一举动的进展的 OP 有什么建议?
  • @Çöđěxěŕ:对于同一卷内的移动,完全不需要进度条。如果他们关心跨卷案例并且仍然想要一个进度条,还有其他选择,包括自己实施副本,但首先他们需要先解决他们需要帮助的问题。
  • 我想首先解决的确切问题是,在保持某种进度跟踪器的同时复制速度相当慢。我现有的代码显示了我尝试过的内容。但是,如果需要,可以更改任何内容以获得更好的结果。我将编辑我的帖子,所以我只问一个问题。

标签: c# wpf file-management


【解决方案1】:

移动操作非常慢,我找不到让它更快的解决方案

移动文件需要这么长时间的原因可能有很多。例如:反恶意软件应用程序 - 可能扫描文件、网络负载(如果移动到另一个卷/驱动器)、文件大小本身,以及可能的代码异味。

我的猜测是我认为您采用了您对代码所做的方式,因此您可以处理到目前为止已经移动了多少,这很好,但是有替代方法 可以移动这些文件就好了,而且更快。

几个选项

  1. System.IO.File.Move() 方法 - 这很好用,但您也无法控制进度。在hood 下,它实际上调用了:Win32Native.MoveFile c++ 函数,效果很好。
  2. FileInfo.MoveTo - 这最终也将其工作委托给了Win32.MoveFile
  3. 您的方式 - 使用 Kernel32.dll 中的一些功能 - 这允许完全控制、进度等...

我会在一分钟后回到上面的这些内容,因为我想根据你最初发布的关于进度没有更早更新的内容来谈谈。

此处的此调用 await Task.Run(() => { mover.Copy(); }); 将一直等待,直到完成,但您在此之后注册事件,例如:mover.OnProgressChanged += percentage => 是在 Copy() 调用之后,所以不,您不会得到任何更改。

即使您收到更改,您也会有异常,因为您不在 UI 线程上,而是在另一个线程上。例如:

 mover.OnProgressChanged += percentage =>
 {
    MoveProgress.Value = percentage;
    InputButtonText.Text = percentage.ToString();
 };

您正在尝试从另一个线程更新 UI (progressbar.value),但您根本无法执行此操作。要解决这个问题,您需要从Dispatcher 调用。例如:

 Application.Current.Dispatcher.Invoke(() =>
 {
    pbProgress.Value = percentage;
 });

返回文件操作

老实说,你仍然可以按照自己的方式做你想做的事,只需移动一些东西,你应该会很好。否则,我在下面编写了一个类,您可以在其中使用它来移动文件、报告进度等。请参见下文。

注意:我测试了一个 500MB 的文件,它在 2.78 秒内移动,一个 850MB 的文件在 3.37 秒内从本地驱动器移动到另一个卷。

 using System;
 using System.IO;
 using System.Runtime.InteropServices;
 using System.Threading.Tasks;
 using System.Transactions; // must add reference to System.Transactions     

public class FileHelper
    {
        #region | Public Events |

        /// <summary>
        /// Occurs when any progress changes occur with file.
        /// </summary>
        public event ProgressChangeDelegate OnProgressChanged;

        /// <summary>
        /// Occurs when file process has been completed.
        /// </summary>
        public event OnCompleteDelegate OnComplete;

        #endregion

        #region | Enums |

        [Flags]
        enum MoveFileFlags : uint
        {
        MOVE_FILE_REPLACE_EXISTSING = 0x00000001,
        MOVE_FILE_COPY_ALLOWED = 0x00000002,
        MOVE_FILE_DELAY_UNTIL_REBOOT = 0x00000004,
        MOVE_FILE_WRITE_THROUGH = 0x00000008,
        MOVE_FILE_CREATE_HARDLINK = 0x00000010,
        MOVE_FILE_FAIL_IF_NOT_TRACKABLE = 0x00000020
        }

        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
        }

        #endregion

        #region | Delegates |

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

        public delegate void ProgressChangeDelegate(double percentage);

        public delegate void OnCompleteDelegate(bool completed);

        #endregion

        #region | Imports |

        [DllImport("Kernel32.dll")]
        private static extern bool CloseHandle(IntPtr handle);

        [DllImport("Kernel32.dll")]
        private static extern bool MoveFileTransactedW([MarshalAs(UnmanagedType.LPWStr)]string existingfile, [MarshalAs(UnmanagedType.LPWStr)]string newfile,
            IntPtr progress, IntPtr lpData, IntPtr flags, IntPtr transaction);

        [DllImport("Kernel32.dll")]
        private static extern bool MoveFileWithProgressA(string existingfile, string newfile,
            CopyProgressRoutine progressRoutine, IntPtr lpData, MoveFileFlags flags);

        [ComImport]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        [Guid("79427A2B-F895-40e0-BE79-B57DC82ED231")]
        private interface IKernelTransaction
        {
            void GetHandle([Out] out IntPtr handle);
        }

        #endregion

        #region | Public Routines |

        /// <summary>
        /// Will attempt to move a file using a transaction, if successful then the source file will be deleted.
        /// </summary>
        /// <param name="existingFile"></param>
        /// <param name="newFile"></param>
        /// <returns></returns>
        public static bool MoveFileTransacted(string existingFile, string newFile)
        {
            bool success = true;
            using (TransactionScope tx = new TransactionScope())
            {
                if (Transaction.Current != null)
                {
                    IKernelTransaction kt = (IKernelTransaction)TransactionInterop.GetDtcTransaction(Transaction.Current);
                    IntPtr txh;
                    kt.GetHandle(out txh);

                    if (txh == IntPtr.Zero) { success = false; return success; }

                    success = MoveFileTransactedW(existingFile, newFile, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, txh);

                    if (success)
                    {
                        tx.Complete();
                    }
                    CloseHandle(txh);
                }
                else
                {
                    try
                    {
                        File.Move(existingFile, newFile);
                        return success;
                    }
                    catch (Exception ex) { success = false; }
                }

                return success;
            }
        }

        /// <summary>
        /// Attempts to move a file from one destination to another. If it succeeds, then the source
        /// file is deleted after successful move.
        /// </summary>
        /// <param name="fileToMove"></param>
        /// <param name="newFilePath"></param>
        /// <returns></returns>
        public async Task<bool> MoveFileAsyncWithProgress(string fileToMove, string newFilePath)
        {
            bool success = false;

            try
            {
                await Task.Run(() =>
                {
                    success = MoveFileWithProgressA(fileToMove, newFilePath, new CopyProgressRoutine(CopyProgressHandler), IntPtr.Zero, MoveFileFlags .MOVE_FILE_REPLACE_EXISTSING|MoveFileFlags.MOVE_FILE_WRITE_THROUGH|MoveFileFlags.MOVE_FILE_COPY_ALLOWED);
                });
            }
            catch (Exception ex)
            {
                success = false;
            }
            finally
            {
                OnComplete(success);
            }

            return success;
        }

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

            return CopyProgressResult.PROGRESS_CONTINUE;
        }

        #endregion
    }

如何使用

一个例子 -

 // Just a public property to hold an instance we need
 public FileHelper FileHelper { get; set; }

加载注册事件...

 FileHelper = new FileHelper();
 FileHelper.OnProgressChanged += FileHelper_OnProgressChanged;
 FileHelper.OnComplete += FileHelper_OnComplete;

这是逻辑...

 private async void Button_Click(object sender, RoutedEventArgs e)
 {
    bool success = await FileHelper.MoveFileAsyncWithProgress("FILETOMOVE", "DestinationFilePath");
 }

 // This is called when progress changes, if file is small, it
 // may not even hit this.
 private void FileHelper_OnProgressChanged(double percentage)
 {
        Application.Current.Dispatcher.Invoke(() =>
        {
            pbProgress.Value = percentage;
        });
 }

 // This is called after a move, whether it succeeds or not
 private void FileHelper_OnComplete(bool completed)
 {
        Application.Current.Dispatcher.Invoke(() =>
        {
            MessageBox.Show("File process succeded: " + completed.ToString());
        });
 }

*注意:那个辅助类中还有另一个函数MoveFileTransacted,你真的不需要这个,它是另一个允许你使用事务移动文件的辅助函数;如果发生异常,文件不会移动等...

【讨论】:

  • 那是一个地狱般的解释,我想我明白了大部分。但是该死的,这比我预期的要复杂得多(看起来)。非常感谢你做的这些!我把它手写了一遍,并确保我理解了它所说的内容。
  • 虽然我不得不承认 DLL 导入让我非常困惑。
  • 我正在移动一个大约 2 GB 的文件,大约需要 25 秒。我对它的速度有多快感到非常惊讶!下一步将是观察文件系统中的变化并自动化该过程。如果要移动多个文件,此代码会产生问题吗?我预计会显着放缓。回复有点晚了,现在上班睡觉了哈哈
  • @TorbenVanAssche 我认为如果移动多个文件不会引起问题,但实施和测试应该是最少的。所有主要工作都是在一个新线程上完成的,当然,如果您有数千人参加,您可能会看到一些减速,但很可能不会是您的情况。
  • 酷。是时候开始使用我的文件系统观察器了。
猜你喜欢
  • 2012-07-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-12-31
  • 2021-10-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多