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

编写文件夹同步应用程序

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.18/5 (7投票s)

2010年11月8日

CPOL

8分钟阅读

viewsIcon

78602

downloadIcon

8973

本文使用轻量级线程、互斥锁和简单的算法来确定要同步的文件。

开始使用

对于希望在不使用源代码的情况下测试该应用程序的用户,请运行下载的安装程序并安装该应用程序,然后通过单击桌面上的快捷方式来运行该应用程序,打开第二个选项卡,然后单击位于下方的“添加”按钮。手动输入源文件夹位置和目标位置,然后单击“保存”。等待处理窗口关闭,然后单击“隐藏”按钮。然后,该应用程序将在后台同步这两个文件夹。请注意,两个文件夹都必须存在;如果其中一个或两个文件夹不存在,应用程序将假定其已离线,并继续监视直到其可用。一旦可用,它将开始搜索差异并启动同步任务。

引言

本文附带的应用程序功能齐全。起初,我很困惑,因为我在办公室和家里的电脑上有很多需要备份的文件,但有时我经常忘记我电脑上的文件是否是最新的,还是办公室里的文件是最新的?有时我两台电脑上都进行了更改,所以需要一种方法来同步这两个文件夹!

基于此,我编写了这款名为 FolderSync 的应用程序,这是第一个版本 1.0。该应用程序将同步两个文件夹,可以是双向(从源到目标和从目标到源复制)或单向(仅到目标)。

背景

此应用程序的后台是一个名为 FolderSynchronization 的类,启动后,该类将启动 3 个线程。第一个线程是同步线程,用于弹出队列中的任何新的 FileOperationFileOperation 是一个具有多态 DoOperation() 方法的对象实例,将调用该方法并执行操作,例如将文件从一个文件夹复制到另一个文件夹,或创建文件夹(如果不存在)。这些 FileOperation 对象由第二个线程创建并推送到队列。

第二个线程,即扫描线程,将扫描两个文件夹并搜索任何差异。如果发现差异,它将创建一个 FileOperation 对象并将其推送到队列,以便第一个线程可以进行操作;如果需要同步的文件夹因某种原因不存在(源或目标或两者都),该线程将排队一个监视作业给第三个线程。第三个线程,即监视线程,基本上是从队列中弹出作业并检查源文件夹和目标文件夹是否存在;如果不存在,它将继续监视直到其存在,一旦存在,它将搜索差异并将 FileOperation 对象推送到队列。

Using the Code

首先创建同步对象的实例,然后启动它。之后,将 FolderSynchronizationScannerItem 传递给对象的 AddScan 方法,它将开始扫描和同步这两个文件夹。

FolderSynchronization _Sync = new FolderSynchronization();

 _Sync.Start();

FolderSynchronizationScannerItem fssi = new FolderSynchronizationScannerItem();

fssi.Source = "C:\users\username\desktop\Folder1";

fssi.Destination = "C:\users\username\desktop\Folder2";

fssi.Option = FolderSynchorizationOption.Both;

fssi.Monitor = true;
_Sync.AddScan(fssi);

代码工作原理

如前所述,当您调用 _Sync.Start 时,有 3 个线程正在运行,当您调用 _Sync.AddScan 时,当前运行的线程会将 FolderSynchronizationScannerItem 添加到 _ScanQueue 中。此扫描队列将由扫描线程弹出,当发生出队时,扫描线程将调用 FolderSynchronizationScanner 类中的 Sync() 方法,并在该线程上继续工作,直到找到所有目录和子目录,并返回一个 MultiKeyCollection<FileOperation>MultiKeyCollection 类基本上是 Dictionary<string, Object> 的包装器,但区别在于它将从类属性创建键(string)。如果您使用 string[] {"PropA", "PropB", "PropC"} 创建 MultiKeyCollection,每次向集合添加对象时,它将从该对象的属性 PropABC 创建一个键,并且该对象必须具有这些属性。

然后,扫描线程将循环并将 FolderSynchronizationScanner 返回的所有 FileOperation 添加到 FolderSynchronization 类中的 _SyncQueue 中。同步线程将自动将其出队并在后台处理。

扫描过程中有趣的部分是这个方法。

protected void StartFolder(string folder, int level)
        {
            string sourcePath = Path.Combine(_Source, folder);
            string destinationPath = Path.Combine(_Destination, folder);

            if (Directory.Exists(sourcePath) == false) AddCreateFolderTask(sourcePath);
            if (Directory.Exists(destinationPath) == false) 
				AddCreateFolderTask(destinationPath);

            if (Directory.Exists(sourcePath) == true) 
		LoadFilesInFirstPath(sourcePath, destinationPath, false);
            if (_Options == FolderSynchorizationOption.Both && 
		Directory.Exists(destinationPath) == true) 
			LoadFilesInFirstPath(destinationPath, sourcePath, true);

            if (Directory.Exists(sourcePath) == true)
            {
                foreach (string subfolder in Directory.GetDirectories(sourcePath))
                {
                    string shortfoldername = subfolder.GetLastPathName(level + 1);
                    StartFolder(shortfoldername, level + 1);
                }
            }

            if (_Options == FolderSynchorizationOption.Both && 
			Directory.Exists(destinationPath) == true)
            {
                foreach (string subfolder in Directory.GetDirectories(destinationPath))
                {
                    string shortfoldername = subfolder.GetLastPathName(level + 1);
                    StartFolder(shortfoldername, level + 1);
                }
            }
        } 

调用 AddScan() 时将调用此方法。此方法基本上是在两个文件夹之间搜索差异,并指定选项,无论是“Both”还是“Destination”同步选项。第一次调用 StartFolder() 时,它将使用 StartFolder("", -1) 调用,这基本上是根文件夹,值 -1 用于获取指定路径的最后一个文件夹名称,例如调用方法 stringObject.GetLastPathName(0),其中 stringObject 是“c:\users\username\John”,将返回“John”;如果调用级别为 1,则返回“username\John”等等。此扩展方法用于以 string 形式获取指定路径的最后几个子目录。该 string 稍后将与源文件夹和目标文件夹连接起来以检查目录是否存在;如果不存在,它将添加一个 FileOperation 任务来创建文件夹。

文件比较是通过使用 ComponentLibrary 来完成的,这个 ComponentLibrary 本身就值得单独讨论,但在此简要说明,这个 ComponentLibrary 基本上是查询文件夹中的所有 DLL 文件,并检查 DLL 的任何类型是否与模板类匹配,代码如下:

ComponentLibrary<IFileComparer> libraries = new ComponentLibrary<IFileComparer>();

执行上述代码时,它将在可执行文件目录中搜索所有 DLL 文件,并检查 DLL 文件中的任何类型是否实现了 IFileComparer。如果找到,它将创建该类型的实例,并将其添加到集合中。然后,这个库类将被包装在名为 MultiFileComparer 的类中。代码如下:

MultiFileComparer<ifilecomparer> _Comparer = 
		new MultiFileComparer<ifilecomparer>(libraries);

这个类基本上是 ComponentLibrary 类的包装器。这个 MultiFileComparer 类有一个名为 Compare() 的方法,它将循环遍历 ComponentLibrary 中实现 IFileComparer 的所有实例,并执行这些实例的 Compare() 方法,然后将结果相加并返回结果。如果结果为 < 0,则意味着源文件需要被覆盖;如果结果为 > 0,则意味着目标文件需要被覆盖;而 0 表示文件相同。DefaultSyncComparer 类是实现 IFileComparer 的类,它位于 FolderSync.Library.Comparer 项目中。它位于另一个 DLL 中,因为它是可扩展的,允许程序员编写自己的规范来决定何时覆盖文件。这是 MultiFileComparer 类的源代码:

 public class MultiFileComparer<t>
    {
        protected ComponentLibrary<t> _ComponentList = null;

        public MultiFileComparer(ComponentLibrary<t> _componentList)
        {
            _ComponentList = _componentList;
        }
        
        public int Compare(FileInfo file1, FileInfo file2)
        {
            int result = 0;
            if (_ComponentList == null || _ComponentList.Components.Count <= 0) 
		return result;
            foreach (IFileComparer _comparer in _ComponentList.Components)
            {
                result = result + _comparer.Compare(file1, file2);
            }
            return result;
        }
    }

基本上,MultiFileComparer 将添加所有 IFileComparer 实例生成的所有值,最终值将用作结果(无论是 <0 还是 >0)。这意味着您可以创建一个返回例如 -1000 的高值的类型,因为该条件绝对必须覆盖源文件,当它与另一个类型相加时,仍然会得到负值。这意味着您可以通过返回不同值来创建自己的文件比较条件扩展。

同步工作原理

基本上,当调用 StartFolder() 时,它将遍历源文件夹的所有文件和所有子目录,并检查目标是否存在;如果不存在,它将创建文件/目录;如果存在,它将检查哪个文件的日期最新。DefaultSyncComparer 只检查最新日期,而不检查大小;您可以扩展它,使用自己的逻辑,通过 FolderSync.Library.Comparer 来实现,然后告诉我。当源文件夹的所有文件和目录与目标文件夹进行比较后,这意味着我们只完成了一半,目标文件夹可能包含源文件夹中没有的文件或文件夹。当 Sync Option 在 FolderSynchronizationScannerItem.Option 属性中指定为“Both”时,它还将循环遍历目标文件夹的所有文件和文件夹,并将其与源进行比较。

例如,我们有这种情况:

  • 源文件夹有一个文件 a.txt,日期为 2011 年 1 月 1 日。
  • 目标文件夹也有一个文件 a.txt,日期为 2011 年 1 月 2 日。

当调用第一个循环时,它将获取源文件夹中的所有文件,包括 a.txt。然后检查目标文件夹,发现源文件夹需要被覆盖。它将创建一个 FileOperation 对象并将其添加到集合中,集合会检查是否没有类似的键存在,然后将其添加到集合中。键是通过 SourceFileName 连接分隔符“#”然后连接 DestinationFileName 来创建的。

然后调用第二个循环,这次参数是目标到源。它将比较目标 a.txt 和源 a.txt。然后确定相同的结果,即源文件需要被覆盖。当它调用 _SyncCollection.GetAddObject 时,_SyncCollection 类型(即 MultiKeyCollection)将确定已存在具有相同键的对象,并将忽略第二个 FileOperation 任务,而不将其添加到集合中,这样文件就不会被复制两次。您可能需要参考 FolderSynchronizationScanner.cs 中的源代码以获得更清晰的说明。第二个循环很重要,当目标文件夹中存在一个文件,而源文件夹中不存在时。在这种情况下,将创建一个 FileOperation 任务来将目标文件夹文件复制到源文件夹文件。

关注点

如果您有任何输入、想法或改进意见,请发送电子邮件至 jokenjp@yahoo.com,请在主题中加上 [FolderSync]

经过测试,我发现了一些与 UAC 相关的错误。有人能帮我解决一下吗?我计划将应用程序模拟为登录用户,这样应用程序可能需要询问密码,因为如果每次在启动时都提示用户以管理员身份运行应用程序,那就太麻烦了,谁有任何想法?谢谢。

历史

这是第一个版本 1.0。功能包括:

  1. 开机自启动
  2. 在后台同步两个文件夹
  3. 单实例应用程序
  4. 可扩展,同步确定算法可扩展
  5. 完整的面向对象代码
© . All rights reserved.