目录
介绍
使用代码
用法
算法
Step 类
文件/文件夹和步骤枚举–使用LINQ进行对象
Browsers
Backgroundworkers
痛点
枚举文件
单线程与多线程
空闲空间计算的差异
目标文件夹已存在
由于进度变化太多,导致应用冻结
- 下载源文件-1.1 MB
智能分区文件交换是Windows实用程序,可自动执行在文件系统中两个不同分区之间交换数据的过程。它对于非常接近满容量的分区之间的文件交换过程的自动化特别有用——实际上,这首先是该项目的主要目标。
通常,在分区上的可用空间足以将所有文件从分区A复制到分区B,然后再从分区B复制到分区A的情况下,这需要两个步骤,而且很容易手动完成。但是,如果出现更频繁的情况,即分区上没有足够的空间来一次性传输所有文件,则必须以较小的批次传输文件,即A-> B然后B-> A然后再次A- -> B等。下图解释了该概念:
该实用程序将自动计算所有必要步骤,并一键完成所有步骤。
使用代码 用法用户首先选择他/她想要在其之间交换文件/文件夹结构的路径。它们既可以写在顶部的文本框中,也可以使用“浏览”按钮进行选择。
选择/输入路径后,其内容将显示在Webbrowser控件下的textbox,标签将显示分区的当前使用情况信息。注意:在某些情况下,文件大小会花费更长的时间进行计算(如果文件数量很多)。在这些情况下,Size标签将显示“ (… calculating …)”,并且传输开始将被禁用,直到计算完成。
在选择了两个路径并计算了它们的大小之后,用户可以按下“START TRANSFER”按钮。这将触发计算并随后执行步骤。
注意:还有一个“Simulation” checkbox,当前已默认选中它。checkbox选中此选项后,将不会实际传输文件夹和文件,仅会计算和模拟步骤。通过“传输速度 ” textbox,您可以输入所需的模拟传输速度(如果为空或为零,则将使用最大速度)。
传输开始后,“START TRANSFER”(开始传输)按钮上方将出现另一个按钮-“Cancel”(取消)按钮。通过单击此按钮,您可以选择还原所有更改,或者完全取消传输并将已传输的文件保留在新位置。
传输过程开始后,执行过程分为几个阶段。
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类。
该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_FOLDERS和DELETE_FOLDERS步骤中使用。
SourceFile和DestinationFile是MOVE_FILES和RENAME_FILES步骤中使用的文件路径。
FileSize是要传输的文件的大小(也在MOVE_FILES和RENAME_FILES步骤中使用)。
Iteration标记该步骤的迭代。仅MOVE_FILES步骤将分配迭代。
StepDonetrue一旦执行该步骤,就会将其标记。还原执行还会重置此属性。
最后,folderExists是一个private属性,它将控制是否创建目录(实际上,如果它已经存在于目标上)。
Step 类还定义了以下方法:
public void DoStep();
public void RevertStep();
public string PrintStepXml(ref FileStream stream);
方法DoStep()和RevertStep()定义如何根据步骤的ProgressStage属性(正向或反向)执行步骤。例如,DoStep()将移动SourceFile到DestinationFile,而RevertStep()将移动DestinationFile到SourceFile。DoStep()将设置StepDone为true,而RevertStep()将设置StepDone为false。
PrintStepXml(ref FileStream stream)方法以XML格式的字符串返回Step对象的属性,并将其写到提供的FileStream中。这用于调试目的。
步骤应收集在这样声明的词典中:
private Dictionary _steps;
(_steps Dictionary是一个全局变量,并且是收集和执行所有步骤的主字典。Dictionary在计算过程中还使用了这样的几个其他临时对象。)
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);
首先,所有目录都是从两个路径枚举的,但是切换了它们的基本路径;path1到path2,反之亦然。然后,排除系统文件夹。文件夹以升序排序,以确保在其子文件夹之前创建父文件夹(即X:\folder\subfolder之前的X:\folder)。最后,Step创建对象并将枚举强制转换为Dictionary。
MOVE_FILES步骤
MOVE_FILES计算是要复杂一点,因为它试图模拟两路之间的文件传输,并试图在尽可能少的迭代中完成这项工作。这是通过称为的CalculateIterations方法来完成的。
private Dictionary CalculateIterations(TRANSFER_DIRECTION direction);
该方法采用TRANSFER_DIRECTION作为参数,该参数是具有2个选择的enum——LEFT2RIGHT和RIGHT2LEFT——标记传输过程模仿是从path1(左)到path2(右)还是以其他方式开始。该方法将在单独的线程中调用两次,每个选项一个线程。为此,使用了异步运行的Task对象来同时检查两个选项。调用者方法将暂停,直到完成第一个(任何一个)Task对象为止。
Task[] tasks = new Task[2];
Dictionary _steps1 = new Dictionary();
Dictionary _steps2 = new Dictionary();
Dictionary _steps_move = new Dictionary();
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步骤
这些步骤是在贯穿所有MOVE_FILES步骤的for循环中计算的,并检查目标文件是否存在。如果是这样,它将在目标文件名中添加一个唯一的后缀,并在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 _steps_rename_back = new Dictionary();
// for all the files to be moved
for (step_nr = step_nr_min; _steps_move.ContainsKey(step_nr) &&
step_nr 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);
首先,列举来自path1和path2的所有文件夹。然后是要创建的文件夹被删除(即,如果有两个相同的文件夹名称path1和path2-然后在此文件夹应存放在两个位置)。然后排除系统文件夹。使用降序排列,以便在父级之前删除子级。Step词典终于创建。
为了直观地显示源/目标文件系统位置的内容以及传输进度本身,使用了两个WebBrowser控件。在传输过程中,名为backgroundWorkerRefresh的BackgroundWorker会不断刷新它们,以及它们上面的大小和可用空间标签。
每个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对象共享处理事件相同的DoWork和RunWorkerCompleted方法,因为它们基本上做相同的事情,除了第一个用于path1的计算,后者用于path2的计算。
它们都在每次调用RefreshSizeFreeSpace方法时都运行,也就是每次任何位置更改时(或者从技术上讲,当路径textbox失去焦点时都运行)。他们计算已用和可用空间,并枚举文件,每个文件分别用于各自的路径。
BackgroundWorkerRefresh
该BW对象会定期刷新标签大小和可用空间,并在文件传输操作期间刷新WebBrowser对象。它还负责剩余时间的计算。剩余时间的计算公式为:
TIME_REMAINING = SIZE_OF_REMAINING_FILES / (SIZE_OF_TRANSFERRED_FILES / TIME_ELAPSED)
BackgroundWorkerRefresh与backgroundWorkerFileTransfer同时启动,并在文件传输完成后取消(停止)。
BackgroundWorkerFileTransfer
backgroundWorkerFileTransfer是整个项目的核心对象。它完成了所有重要的工作。
它由buttonStart.Click事件触发,并执行前面描述的算法的所有步骤。
在准备阶段,它将为每个路径调用CalculateFilesAndSize方法,该方法枚举文件并计算磁盘使用率和可用空间。如果此方法失败,则会报告错误。
在接下来的几个阶段中,BW收集必要的步骤以正确执行文件传输。它使用LINQ to Object方法来填充_steps字典,所有这些都已在前面进行了描述。
所有的步骤都列举之后,该方法会使用Step的对象的WriteStepXml方法,以写出所有的步骤到XML文件——用于调试的目的。
之后是执行部分。
步骤的执行是在一个for循环中完成的,该循环以指定的顺序一个接一个地循环遍历所有Step对象中的_steps字典。如果是_revertback = true,它将执行Step对象的RevertStep()方法,否则将执行DoStep()方法。如果发生异常,它将为用户提供3种选择——中止/重试/忽略——并根据DialogResult,将重试最后一步,忽略并继续下一步,或者在中止的情况下,询问用户是否它应该还原所有步骤——如果是,它将设置_revertback = true和还原计数器,以使for循环现在向后计数——并通过调用RevertStep()方法还原已完成的所有步骤。
此BW还刷新两条路径的size和freespace全局变量,刷新labelIterations并使用方法GetMessage写出UI和日志消息。
痛点 枚举文件通常,将使用System.IO.Directory.NET库方法枚举Windows系统上的系统条目。但是,.NET 中的标准Enumerate方法存在一个缺陷,该缺陷会导致方法在碰到系统文件或文件夹时抛出一个“Exception”。例如,如果要枚举分区根目录(即D:\)上的文件和/或文件夹,则该Enumerate方法最终将运行到$ RECYCLEBIN文件夹中,并且将中断,仅返回部分filesystem条目枚举。
为解决此问题,我创建了一个名为System.IO.DirectoryAlternative的库替代System.IO.Directory库,该库使用与原始库相同的WinAPI函数,但没有所描述的缺陷,并且运行速度比.NET版本快。
单线程与多线程在单线程操作中,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()));
ProgressChanged方法只留下progressbar进行更新,而且它只在离散的时隙中进行,在大多数情况下,低于1%的更改在ProgressBar中是不可见的。
// 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;
}