65.9K
CodeProject 正在变化。 阅读更多。
Home

智能分区文件交换

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2019年12月22日

CPOL

13分钟阅读

viewsIcon

10789

downloadIcon

205

一个用于自动将文件在分区 A 和分区 B 之间移动的实用程序

引言

Smart Partition File Exchange 是一个 Windows 实用程序,可自动执行文件系统在两个不同分区之间交换数据的过程。它对于自动化非常有用,特别是对于容量接近满的文件分区之间的文件交换过程——事实上,这正是本项目最初的主要目标。

通常,在分区的可用空间足以将所有文件从分区 A 复制到分区 B,然后再反之亦然的情况下,这需要两个步骤,并且手动完成起来很容易。但考虑到一个更常见的情况,即分区上没有足够的空间一次性传输所有文件,您将不得不分批传输文件,A --> B,然后 B --> A,再 A --> B,依此类推。下图解释了该概念。

此实用程序将自动计算所有必要的步骤,并一键完成所有操作。

Using the Code

用法

用户首先选择他们想要交换文件/文件夹结构的目标路径。这些路径可以直接在顶部的文本框中输入,或者通过点击“浏览”按钮来选择。

一旦选择了/输入了路径,其内容将显示在文本框下方的Webbrowser控件中,标签将显示分区的当前使用信息。注意:在某些情况下,计算大小可能需要更长的时间(如果文件数量很大)。在这种情况下,Size标签将显示“(…正在计算…)”,并且在计算完成之前,传输启动将被禁用。

在选择了两个路径并计算完它们的大小后,用户可以按“开始传输”按钮。这将触发步骤的计算和随后的执行。

注意:还有一个“模拟复选框,目前默认选中。当选中此复选框时,不会实际传输文件夹和文件,只会计算并模拟步骤。通过“传输速度文本框,您可以输入模拟传输所需的期望速度(如果留空或为零,则使用最大速度)。

传输开始后,“开始传输”按钮上方将出现一个额外的按钮——“取消”按钮。点击此按钮,您可以选择撤销所有更改,或者仅完全取消传输并将已传输的文件保留在其新位置。

算法

传输过程开始后,有几个执行阶段。

  1. 准备 – 在此阶段,将枚举两个位置的文件并计算其大小。
  2. * 收集步骤 – 创建文件夹 – 首先需要创建文件夹,因为目录结构必须存在才能移动文件,否则,如果目标文件夹不存在,移动操作将导致异常。在此部分代码中,将枚举两个路径下的子文件夹,并切换它们的基础路径字符串。

    即,

    (If path1 = X:\ and path2 = Y:\)
    	X:\dir\subdir => [CREATE] Y:\dir\subdir
    	Y:\another_dir\another_subdir => [CREATE] X:\another_dir\another_subdir
  3. * 收集步骤 – 移动文件 – 在创建了正确的目录结构后,就可以移动文件了。在此步骤中,将枚举两个路径下的文件,并切换它们路径字符串中的基础路径。调用CalculateIterations方法,该方法尝试计算传输所有文件的最小步骤数。

    即,

    (If path1 = X:\ and path2 = Y:\)
    	X:\dir\subdir\file1.txt => [MOVE TO] Y:\dir\subdir\file1.txt
    	Y:\another_dir\another_subdir\file2.txt => 
        [MOVE TO] X:\another_dir\another_subdir\file2.txt
  4. * 收集步骤 – 重命名文件 – 在这一点上,有可能一些目标文件名与目标路径中已有的文件名匹配。为此,对于这些文件中的每一个,将创建一个新的临时目标文件名,并创建一个额外的步骤,以便在稍后将临时文件名重命名回原始文件名。

    即,

    (If path1 = X:\ and path2 = Y:\)
    	X:\file1.txt => [MOVE TO] Y:\file1.txt !!! Y:\file1.txt ALREADY EXISTS !!!
    	X:\file1.txt => [MOVE TO] Y:\file1.txt_somespecialidentifier.tmp
    	Additional step (in the end): 
              Y:\ file1.txt_somespecialidentifier.tmp => [RENAME TO] Y:\file1.txt
  5. * 收集步骤 – 删除文件夹 – 文件移动完成后,是时候清理了!在此阶段,将枚举两个路径下的源文件夹,并将其标记为在所有文件移动后删除(因为此时这些文件夹将为空)。
  6. 将步骤写入 XML(用于调试)
  7. * 执行 – 步骤按给定顺序执行;它们通过一个for循环进行迭代,并且该循环还会处理执行过程中可能发生的任何错误 + 如果按下取消按钮,将询问用户是否需要撤销更改,如果需要,则循环将被反转,并且步骤将按相反的顺序执行,从中断点一直向下到第一个步骤。

标记为*的阶段将在后续文本中更详细地解释。

步骤类

使用的最重要的数据结构是Step类。

Step对象具有以下属性:

public PROGRESS_STAGE ProgressStage;
public string FolderName;
public string SourceFile;
public string DestinationFile;
public long FileSize;
public int Iteration;
public bool StepDone = false;
private bool folderExists = false;

ProgressStage属性标记Step对象的“类型”。PROGRESS_STAGE是一个enum,具有以下成员:

enum PROGRESS_STAGE 
{ PREP_CALC, PREP_CALC_DONE, CREATE_FOLDERS, MOVE_FILES, RENAME_FILES, DELETE_FOLDERS };

FolderName是文件夹路径 – 用于CREATE_FOLDERSDELETE_FOLDERS步骤。

SourceFileDestinationFile是用于MOVE_FILESRENAME_FILES步骤的文件路径。

FileSize是正在传输的文件的大小(也用于MOVE_FILESRENAME_FILES步骤)。

Iteration标记步骤的迭代次数。只有MOVE_FILES步骤会有迭代次数。

StepDone在步骤执行后标记为true。撤销执行也会重置此属性。

最后,folderExists是一个private属性,它将控制是否创建目录(如果该目录已存在于目标位置)。

Step类还定义了以下方法:

public void DoStep();
public void RevertStep();
public string PrintStepXml(ref FileStream stream);

DoStep()RevertStep()方法根据ProgressStage属性定义了如何在正常或反向方向上执行一个步骤。例如,DoStep()会将SourceFile移动到DestinationFile,而RevertStep()会将DestinationFile移回SourceFileDoStep()会将StepDone设置为true,而RevertStep()会将StepDone设置为false

PrintStepXml(ref FileStream stream)方法以 XML 格式字符串返回Step对象的属性,并将其写入提供的FileStream。这用于调试目的。

步骤将收集在声明为这样的字典中:

private Dictionary<int, Step> _steps;

_steps Dictionary是一个全局变量,也是所有步骤将从其收集和执行的主要字典。在计算过程中,还有几个临时的Dictionary对象,如这个。)

文件/文件夹和步骤枚举 – 使用 LINQ to Objects

Step对象在几个计算阶段(已在“算法”章节中描述)被创建。对于大多数Step集合,都使用 LINQ to Objects 来获取枚举。

CREATE_FOLDERS 步骤

_steps = _steps.Concat(

                DirectoryAlternative.EnumerateDirectories(_path1, "*", 
                SearchOption.AllDirectories).Select(x => x.Replace
                (_drive1, _drive2)) // take all folders from path1 and switch drive name
                .Concat(DirectoryAlternative.EnumerateDirectories
                (_path2, "*", SearchOption.AllDirectories).Select
                (x => x.Replace(_drive2, _drive1))) // take all folders from 
                                                    // path1 and switch drive name
                .Where(x => !x.Contains(RECYCLE_BIN) && 
                  !x.Contains(SYSTEM_VOLUME_INFORMATION)) // exclude system folders
                .OrderBy(x => x) // ascending order so that first the upper level folders 
                                 // are created and then subfolders
                .ToDictionary(k => step_nr++, v => new Step
                             (PROGRESS_STAGE.CREATE_FOLDERS, v)) // add step_nr 
                                                                 // and CREATE_FOLDERS flag
                ).ToDictionary(k => k.Key, v => v.Value);

首先,从两个路径中枚举所有目录,但会切换它们的基础路径;path1path2,反之亦然。然后,排除系统文件夹。文件夹按升序排序,以确保父文件夹在子文件夹之前创建(即,X:\folderX:\folder\subfolder 之前)。最后,创建Step对象并将枚举转换为Dictionary

MOVE_FILES 步骤

MOVE_FILES的计算要复杂一些,因为它试图模拟两个路径之间的文件传输,并尽量用最少的迭代次数来完成工作。这在一个名为CalculateIterations的方法中完成。

private Dictionary<int, Step> CalculateIterations(TRANSFER_DIRECTION direction);

该方法接受TRANSFER_DIRECTION作为参数,这是一个具有2个选项的enumLEFT2RIGHTRIGHT2LEFT – 表示传输过程模拟是从path1(左)到path2(右)开始,还是反之。该方法将在单独的线程中调用两次,一个线程用于一个选项。为此,使用了Task对象,它们是异步运行的,以便同时检查两个选项;调用方法将暂停,直到第一个(任何一个)Task对象完成。

Task[] tasks = new Task[2];

Dictionary<int, Step> _steps1 = new Dictionary<int, Step>();
Dictionary<int, Step> _steps2 = new Dictionary<int, Step>();
Dictionary<int, Step> _steps_move = new Dictionary<int, Step>();

tasks[0] = Task.Factory.StartNew(() => _steps1 = 
                     CalculateIterations(TRANSFER_DIRECTION.LEFT2RIGHT));
tasks[1] = Task.Factory.StartNew(() => _steps2 = 
                     CalculateIterations(TRANSFER_DIRECTION.RIGHT2LEFT));

Task.Factory.ContinueWhenAny(tasks, x =>
{
…
}

RENAME_FILES 步骤

这些步骤是在一个for循环中计算的,该循环遍历所有MOVE_FILES步骤,并检查目标文件是否存在。如果存在,它会向目标文件名添加一个唯一的后缀,并创建一个额外的步骤,在MOVE_FILES步骤完成后执行 – 将临时文件名替换为原始文件名。

string suffix;
int step_nr_max = _steps_move.Max(x => x.Key);
int step_nr_min = _steps_move.Min(x => x.Key);
int i = 1;
Dictionary<int, Step> _steps_rename_back = new Dictionary<int, Step>();

// for all the files to be moved
for (step_nr = step_nr_min; _steps_move.ContainsKey(step_nr) && 
                            step_nr <= step_nr_max; step_nr++)
{
   // check if the destination file already exists
   if (File.Exists(_steps_move[step_nr].DestinationFile))
   {
      // create suffix for new filename
      suffix = "_" + DateTime.Now.ToString("yyyyMMddHHmmssfffffff") + ".tmp";
      // add a step to change the name back afterwards
      _steps_rename_back.Add(
         i++,
         new Step(PROGRESS_STAGE.RENAME_FILES, null, 
         _steps_move[step_nr].DestinationFile + suffix, _steps_move[step_nr].DestinationFile)
      );
      // change the destination filename for current step
      _steps_move[step_nr].DestinationFile += suffix;
      Thread.Sleep(1);
   }
}

DELETE_FOLDERS 步骤

_steps = _steps.Concat(
   DirectoryAlternative.EnumerateDirectories
      (_path1, "*", SearchOption.AllDirectories)  // take all folders from path1
   .Concat(DirectoryAlternative.EnumerateDirectories
      (_path2, "*", SearchOption.AllDirectories)) // concatenate with all folders from path2
   .Except(_steps.Values.Where(x => x.ProgressStage == 
      PROGRESS_STAGE.CREATE_FOLDERS).Select(x => x.FolderName)) // except folders that are 
                                                                // to be created
   .Where(x => !x.Contains(RECYCLE_BIN) && 
          !x.Contains(SYSTEM_VOLUME_INFORMATION)) // exclude system folders
   .OrderByDescending(x => x) // descending order so that subfolders come first 
                              // (otherwise we get folder not empty exception)
   .ToDictionary(k => step_nr++, 
    v => new Step(PROGRESS_STAGE.DELETE_FOLDERS, v)) // add step_nr and DELETE_FOLDERS flag
).ToDictionary(k => k.Key, v => v.Value);

首先,枚举path1path2下的所有文件夹。然后删除要创建的文件夹(即,如果path1path2上存在同名文件夹 – 则该文件夹应保留在两个位置)。然后排除系统文件夹。使用降序排列,以便子文件夹在父文件夹之前被删除。最后创建Step字典。

浏览器

为了可视化地显示源/目标文件系统位置的内容以及传输进度本身,使用了两个WebBrowser控件。在传输过程中,它们由一个名为backgroundWorkerRefreshBackgroundWorker对象不断刷新,同时还刷新其上方的尺寸和可用空间标签。

每个WebBrowser上方有两个按钮:

– 重置WebBrowser以显示原始路径

– 向上浏览

后台工作者

该项目总共使用了四个BackgroundWorker对象。其中三个用于各种刷新操作,而主backgroundWorkerFileTransfer则负责实际工作。它们共同确保在操作过程中,主线程不会在任何时候变得无响应。

BackgroundWorker对象的初始化在窗体的默认构造函数中完成:

this.backgroundWorkerCalculate1.DoWork += BackgroundWorkerCalculate_DoWork;
this.backgroundWorkerCalculate1.RunWorkerCompleted += 
                                BackgroundWorkerCalculate_RunWorkerCompleted;
this.backgroundWorkerCalculate2.DoWork += BackgroundWorkerCalculate_DoWork;
this.backgroundWorkerCalculate2.RunWorkerCompleted += 
                                BackgroundWorkerCalculate_RunWorkerCompleted;
this.backgroundWorkerFileTransfer.DoWork += BackgroundWorkerFileTransfer_DoWork;
this.backgroundWorkerFileTransfer.ProgressChanged += 
                                BackgroundWorkerFileTransfer_ProgressChanged;
this.backgroundWorkerFileTransfer.RunWorkerCompleted += 
                                BackgroundWorkerFileTransfer_RunWorkerCompleted;
this.backgroundWorkerRefresh.DoWork += BackgroundWorkerRefresh_DoWork;
this.backgroundWorkerRefresh.ProgressChanged += BackgroundWorkerRefresh_ProgressChanged;
this.backgroundWorkerRefresh.RunWorkerCompleted += BackgroundWorkerRefresh_RunWorkerCompleted;

BackgroundWorkerCalculate1 和 BackgroundWorkerCalculate2

这两个 BW 对象共享相同的DoWorkRunWorkerCompleted事件处理方法,因为它们基本上做同样的事情,只是第一个用于path1上的计算,而后者用于path2上的计算。

每次调用RefreshSizeFreeSpace方法时,它们都会被运行,该方法在任何一个位置发生变化时(或者技术上讲,任何一个路径textbox失去焦点时)都会被调用。它们计算各自路径已用空间和可用空间,并枚举文件。

BackgroundWorkerRefresh

此 BW 对象会定期刷新大小和可用空间标签,并在文件传输期间刷新WebBrowser对象。它还负责计算剩余时间。剩余时间计算如下:

TIME_REMAINING = SIZE_OF_REMAINING_FILES / (SIZE_OF_TRANSFERRED_FILES / TIME_ELAPSED)

BackgroundWorkerRefreshbackgroundWorkerFileTransfer同时启动,并在文件传输完成后取消(停止)。

BackgroundWorkerFileTransfer

backgroundWorkerFileTransfer是整个项目的核心对象。它完成所有重要工作。

它由buttonStart.Click事件触发,并执行前面描述的所有算法步骤。

在准备阶段,它为每个路径调用CalculateFilesAndSize方法,该方法枚举文件并计算磁盘使用情况和可用空间。如果此方法失败,将报告错误。

在接下来的几个阶段,BW 收集必要步骤以正确执行文件传输。它使用 LINQ to Object 方法填充_steps字典,这些方法已在前面描述。

在枚举完所有步骤后,该方法将使用Step对象的WriteStepXml方法将所有步骤写入 XML 文件 – 用于调试目的。

之后是执行部分。

步骤执行在一个for循环中完成,该循环按指定顺序逐一遍历_steps字典中的所有Step对象。如果_revertback = true,它将执行Step对象的RevertStep()方法,否则将执行DoStep()方法。如果发生异常,它将向用户提供 3 个选项 – 中止/重试/忽略 – 根据DialogResult,它将重试最后一步,忽略并继续下一步,或者在中止的情况下,询问用户是否需要撤销所有步骤 – 如果是,则将_revertback = true,并反转计数器,以便for循环现在向后计数 – 并且所有已完成的步骤都将通过调用RevertStep()方法来撤销。

此 BW 还刷新两个路径的大小和可用空间全局变量,刷新labelIterations,并使用GetMessage方法写入 UI 和日志消息。

痛点

枚举文件

通常,人们会使用System.IO.Directory .NET 库方法来枚举 Windows 系统上的系统条目。然而,.NET 中的标准Enumerate方法存在一个缺陷,它会在遇到系统文件或文件夹时抛出Exception;例如,如果您正在枚举分区根目录(即 D:\)上的文件和/或文件夹,那么Enumerate方法最终会遇到 $RECYCLEBIN 文件夹,并中断,只返回部分filesystem条目枚举。

为了克服这个问题,我创建了一个替代System.IO.Directory的库,名为System.IO.DirectoryAlternative,它使用与原始库相同的 WinAPI 函数,但没有上述缺陷,并且运行速度也比 .NET 版本快。您可以在以下文章中找到详细描述:

单线程 vs. 多线程

在单线程操作中,在执行处理器密集型操作时,GUI 通常会变得无响应。这就是为什么本项目使用了几个Backgroundworker对象,它们都在单独的线程中工作,以保持 GUI 稳定和响应。这些 BW 对象在前几章中已作介绍。

可用空间计算的差异

由于文件将被移动到的卷的实际可用空间可能与应用程序中计算的可用空间略有不同,因此在计算过程中使用 10 MB 的缓冲区(也可设置为不同值)(即,每个卷始终至少保留 10 MB 的可用空间)。

const long BUFFER_SIZE = 10485760;

目标文件夹已存在

如果源路径和目标路径上存在同名文件夹,则无需创建它,但我们还需要确保在进行恢复时不会删除它。因此,在Step类中引入了一个private属性folderExists;对于目标位置已存在的文件夹,仍会创建一个CREATE_FOLDER步骤,但folderExists属性将被设置为true,这样文件夹实际上就不会被创建。

应用程序因过多进度更改而冻结

如果要移动的文件数量非常多,那么backgroundWorkerFileTransfer将为每个文件报告进度。然而,这导致应用程序变得无响应,消息队列拥堵,直到整个过程结束,都没有消息发送到主线程。

通过使用跨线程将标签消息设置命令移至DoWork事件处理程序,解决了这个问题。这确保了消息与操作同步,并且没有消息被发送到队列。

this.Invoke(new Action(() => labelIterations.Text = _steps[step_nr].Iteration.ToString() + 
            " out of " + _num_iterations.ToString()));

只有progressbar仍然由ProgressChanged方法更新,并且只在离散的时间段内进行更新,因为在大多数情况下,ProgressBar中低于 1% 的变化是无法观察到的。

// report progress every 10ms
if ((_revertback ? _revert_timer : _timer).ElapsedMilliseconds - tick >= PROGRESS_RATE)
{
   backgroundWorkerFileTransfer.ReportProgress(progress, msg.Split('\n')[0]);
   tick = (_revertback ? _revert_timer : _timer).ElapsedMilliseconds;
}

历史

  • 2019年12月22日:初始版本
© . All rights reserved.