智能分区文件交换





5.00/5 (3投票s)
一个用于自动将文件在分区 A 和分区 B 之间移动的实用程序
引言
Smart Partition File Exchange 是一个 Windows 实用程序,可自动执行文件系统在两个不同分区之间交换数据的过程。它对于自动化非常有用,特别是对于容量接近满的文件分区之间的文件交换过程——事实上,这正是本项目最初的主要目标。
通常,在分区的可用空间足以将所有文件从分区 A 复制到分区 B,然后再反之亦然的情况下,这需要两个步骤,并且手动完成起来很容易。但考虑到一个更常见的情况,即分区上没有足够的空间一次性传输所有文件,您将不得不分批传输文件,A --> B,然后 B --> A,再 A --> B,依此类推。下图解释了该概念。
此实用程序将自动计算所有必要的步骤,并一键完成所有操作。
Using the Code
用法
用户首先选择他们想要交换文件/文件夹结构的目标路径。这些路径可以直接在顶部的文本框中输入,或者通过点击“浏览”按钮来选择。
一旦选择了/输入了路径,其内容将显示在文本框
下方的Webbrowser
控件中,标签将显示分区的当前使用信息。注意:在某些情况下,计算大小可能需要更长的时间(如果文件数量很大)。在这种情况下,Size
标签将显示“(…正在计算…)
”,并且在计算完成之前,传输启动将被禁用。
在选择了两个路径并计算完它们的大小后,用户可以按“开始传输”按钮。这将触发步骤的计算和随后的执行。
注意:还有一个“模拟”复选框
,目前默认选中。当选中此复选框
时,不会实际传输文件夹和文件,只会计算并模拟步骤。通过“传输速度”文本框
,您可以输入模拟传输所需的期望速度(如果留空或为零,则使用最大速度)。
传输开始后,“开始传输”按钮上方将出现一个额外的按钮——“取消”按钮。点击此按钮,您可以选择撤销所有更改,或者仅完全取消传输并将已传输的文件保留在其新位置。
算法
传输过程开始后,有几个执行阶段。
- 准备 – 在此阶段,将枚举两个位置的文件并计算其大小。
- * 收集步骤 – 创建文件夹 – 首先需要创建文件夹,因为目录结构必须存在才能移动文件,否则,如果目标文件夹不存在,移动操作将导致异常。在此部分代码中,将枚举两个路径下的子文件夹,并切换它们的基础路径字符串。
即,
(If path1 = X:\ and path2 = Y:\) X:\dir\subdir => [CREATE] Y:\dir\subdir Y:\another_dir\another_subdir => [CREATE] X:\another_dir\another_subdir
- * 收集步骤 – 移动文件 – 在创建了正确的目录结构后,就可以移动文件了。在此步骤中,将枚举两个路径下的文件,并切换它们路径字符串中的基础路径。调用
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
- * 收集步骤 – 重命名文件 – 在这一点上,有可能一些目标文件名与目标路径中已有的文件名匹配。为此,对于这些文件中的每一个,将创建一个新的临时目标文件名,并创建一个额外的步骤,以便在稍后将临时文件名重命名回原始文件名。
即,
(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 - * 收集步骤 – 删除文件夹 – 文件移动完成后,是时候清理了!在此阶段,将枚举两个路径下的源文件夹,并将其标记为在所有文件移动后删除(因为此时这些文件夹将为空)。
- 将步骤写入 XML(用于调试)
- * 执行 – 步骤按给定顺序执行;它们通过一个
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_FOLDERS
和DELETE_FOLDERS
步骤。
SourceFile
和DestinationFile
是用于MOVE_FILES
和RENAME_FILES
步骤的文件路径。
FileSize
是正在传输的文件的大小(也用于MOVE_FILES
和RENAME_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
移回SourceFile
。DoStep()
会将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);
首先,从两个路径中枚举所有目录,但会切换它们的基础路径;path1
到path2
,反之亦然。然后,排除系统文件夹。文件夹按升序排序,以确保父文件夹在子文件夹之前创建(即,X:\folder 在 X:\folder\subfolder 之前)。最后,创建Step
对象并将枚举转换为Dictionary
。
MOVE_FILES 步骤
MOVE_FILES
的计算要复杂一些,因为它试图模拟两个路径之间的文件传输,并尽量用最少的迭代次数来完成工作。这在一个名为CalculateIterations
的方法中完成。
private Dictionary<int, Step> CalculateIterations(TRANSFER_DIRECTION direction);
该方法接受TRANSFER_DIRECTION
作为参数,这是一个具有2个选项的enum
– LEFT2RIGHT
和RIGHT2LEFT
– 表示传输过程模拟是从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);
首先,枚举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
循环中完成,该循环按指定顺序逐一遍历_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日:初始版本